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