View works fine until used in NavigationView/Link

My ContentView has a NavigationView at the top level, then some arbitrary views and then a NavigationLink. I can go into more detail on the code and other views here if someone asks, but I'm not sure it's important right now.


I also have a view called Dots. The somewhat simplified code is:


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)
    var offsets: [CGSize] = []
    
    init(_ quantity: Int) {
        let model = UIDevice().model
        self.quantity = model == "iPad" ? quantity : min(quantity, maxPhoneDots)
    }

    var body: some View {
        VStack {
            GeometryReader { geom in
               // Call to placement algorithm 
                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
           //Some unimportant views
           // ...
        }
    }
}

extension Dots {
    // function using for clarity of calculation done without flooding main function with unnecessary variables/constants
    func calcSeparation(maxLength: CGFloat, dotsNum: Int) -> CGFloat {
        return calcSeparation(maxLength: maxLength, dotsNum: CGFloat(dotsNum))
    }
    
    func calcSeparation(maxLength: CGFloat, dotsNum: CGFloat) -> CGFloat { efforts
        //the max(value, 0) was part of debugging efforts
        let availableLength = max(maxLength - (CGFloat(dotsNum) * diam), 0)
        if availableLength <= 0 { print("WARNING: avail=\(maxLength)")}
        // need +1 separation for inital gap
        return availableLength / CGFloat((dotsNum+1))
    }
    
    func genOffsets(maxWidth: CGFloat, maxHeight: CGFloat) -> [CGSize] {
        // TODO More dynamic limits as f(dims, diam)
        let cols = UIDevice().model == "iPad" ? 8 : 4
        let rows = UIDevice().model == "iPad" ? 13 : 5
        var offsets: [CGSize] = []

// LOOK HERE
        let colSeparation: CGFloat = calcSeparation(maxLength: maxWidth, dotsNum: cols)
        // rowSep, commented

        // 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 free 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
            // ... code for height offest (y) and additional adjustments, currently commented out

            offsets.append(CGSize(width: x, height: 0))
        }
        
        return offsets
    }
}

extension CGSize: Hashable {
    public func hash(into hasher: inout Hasher) {
      hasher.combine(width)
      hasher.combine(height)
    }
}

Line 53 appears to be the cause of a crash due to :

let colSeparation: CGFloat = calcSeparation(maxLength: maxWidth, dotsNum: cols)

The crash also happened when it was a computed property.


Initialising it first (and changing to var) prevents the crash,

var colSeparation: CGFloat = 0
colSeparation = calcSeparation(maxLength: maxWidth, dotsNum: cols)


or alternatively taking the code out of function like so:

let availableLength = max(maxWidth - (CGFloat(cols) * diam), 0)
let colSeparation: CGFloat = availableLength / CGFloat((cols+1))



However, instead of crashing, whilst the individual Dots (pre)view still works as expected, navigating to it (via NavigationView<NavigationLink>) yields an empty screen. Not even the automatically configured "back" navigation appears.


It is something to do with the GeometryReader values as it initialises and updates?

Note this is my first Swift/SwiftUI project.

Accepted Reply

It seems very similar or at least related to this issue https://forums.developer.apple.com/thread/129171#406496


I wrapped the top level in a NavigationView as per the other thread as well as removing the functions declared on line 35 and 39, consequently replacing 54 (and 55) with

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


Finally, to deal with the automatic implications of a NavigationView, I added the modifiers

.navigationBarTitle("")
.navigationBarHidden(true)

to the VStack (ie first view under Nav)


and if on iPad you'll likely want

.navigationViewStyle(StackNavigationViewStyle())

applied to the actual NavigationView.


Since the "fix" required an additional and otherwise irrelevant NavigationView, I'm led to believe this is an actual bug in the SwiftUI framework, but can't be certain.

Replies

It seems very similar or at least related to this issue https://forums.developer.apple.com/thread/129171#406496


I wrapped the top level in a NavigationView as per the other thread as well as removing the functions declared on line 35 and 39, consequently replacing 54 (and 55) with

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


Finally, to deal with the automatic implications of a NavigationView, I added the modifiers

.navigationBarTitle("")
.navigationBarHidden(true)

to the VStack (ie first view under Nav)


and if on iPad you'll likely want

.navigationViewStyle(StackNavigationViewStyle())

applied to the actual NavigationView.


Since the "fix" required an additional and otherwise irrelevant NavigationView, I'm led to believe this is an actual bug in the SwiftUI framework, but can't be certain.