Reputation: 101
This is probably a SwiftUI bug, but I hope someone has a solution for the following problem. I have tried numerous work-arounds but did not find a solution myself unfortunately. I have tested with iOS 15, iOS 16, 16.1, iOS 16.5.1 on devices and on the simulator, the problem is present on all variants.
Given the following full code example:
import SwiftUI
struct Item: Identifiable {
let id: UUID
let name: String
}
struct BugView: View {
@State private var showGrid = false
let items: [Item] = [
.init(id: UUID(), name: "Addition"),
.init(id: UUID(), name: "Subtraction"),
.init(id: UUID(), name: "Multiplication"),
.init(id: UUID(), name: "Division"),
.init(id: UUID(), name: "Times Tables"),
.init(id: UUID(), name: "Division Tables"),
.init(id: UUID(), name: "Positive and negative numbers"),
.init(id: UUID(), name: "Fractions"),
.init(id: UUID(), name: "Add Fractions"),
.init(id: UUID(), name: "Subtract Fractions"),
.init(id: UUID(), name: "Multiply Fractions"),
.init(id: UUID(), name: "Divide Fractions"),
.init(id: UUID(), name: "Fractional Amount"),
.init(id: UUID(), name: "Simplify Fractions"),
.init(id: UUID(), name: "Convert to Fractions"),
.init(id: UUID(), name: "Decimals"),
.init(id: UUID(), name: "Add Decimals"),
.init(id: UUID(), name: "Subtract Decimals"),
.init(id: UUID(), name: "Multiply Decimals"),
.init(id: UUID(), name: "Divide Decimals"),
.init(id: UUID(), name: "Round Decimals"),
.init(id: UUID(), name: "Convert to Decimals"),
.init(id: UUID(), name: "Percentages"),
.init(id: UUID(), name: "Percentages of Amount"),
.init(id: UUID(), name: "Convert to Percentages"),
.init(id: UUID(), name: "Exponents"),
.init(id: UUID(), name: "Roots"),
.init(id: UUID(), name: "Money"),
.init(id: UUID(), name: "Add Money"),
.init(id: UUID(), name: "Subtract Money"),
.init(id: UUID(), name: "Multiply Money"),
.init(id: UUID(), name: "Divide Money"),
.init(id: UUID(), name: "Percentages of Money"),
.init(id: UUID(), name: "Worksheets"),
.init(id: UUID(), name: "Addition Worksheets"),
.init(id: UUID(), name: "Multiplication Worksheets")
]
var body: some View {
if showGrid {
ScrollView(.vertical) {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 160), spacing: 16, alignment: .top)], spacing: 16) {
ForEach(items) { item in
Button {
} label: {
Text(verbatim: item.name)
.padding(48)
}
.buttonStyle(.plain)
}
}
.padding()
}
} else {
Button {
showGrid = true
} label: {
Text(verbatim: "Bug")
}
}
}
}
@main
struct BugApp: App {
var body: some Scene {
WindowGroup {
BugView()
}
}
}
As you can see it is a very minimal example which already contains the bug.
A description of the problem:
When scrolling slowly (this is important), the last two items are sometimes not shown / layed-out. This problem is intermittent. This also only occurs when a Button or NavigationLink is used inside the LazyVGrid as in the example code. When the label of the Button (or NavigationLink) is put in the ForEach without the Button (or NavigationLink), the problem never occurs. When tapping on a NavigationLink with the problem occurring (the last two items not visible), they suddenly pop into view just before the view navigates.
The problem does not occur when the body of the LazyVGrid is:
ForEach(items) { item in
Text(verbatim: item.name)
.padding(48)
}
Update: The problem also seems not to occur when the @State variable is set to true, so the view update in BugView also is a factor in the bug.
What I tried to circumvent the bug:
My thoughts:
This looks like a SwiftUI bug to me which I cannot seem to resolve. I also could not find any documentation or previous issue describing this. Having an .onTapGesture {} on the view (so using no Button or NavigationLink) would solve the bug, but is not a fix for my problem.
From my testing I believe the issue is a lay-out issue in combination with the ScrollView not updating (in time?). Due to the wrapping that occurs with mixed string lengths perhaps with the escaping closure of the Button's or NavigationLink's label, the LazyVGrid might be struggling to set its required space. But I am only guessing about this of course.
What I also found is that when there is no view update, (e.g., setting showGrid = true) then the bug does not seem to appear.
I added a video to show the bug. In the video you see I tap on the button, then scroll down relatively slow. The last item visible is 'Worksheets' which is not the last item in the list. Scrolling back up slightly and scrolling down faster shows the other (correct) last two items:
Do you have any solutions for this issue, or did you find a way around it?
Thank you.
Upvotes: 0
Views: 1160
Reputation: 1
I had that issue with presenting sf symbol Icons in a list, whereby the icons were grouped by certain themes (finance, fitness, activities etc.). Fitness made the largest group and if allocated on top of the grid (Automatically done by LazyVGrid) slowly scrolling up lead to a growing empty space and icons just didn't appear. I thought I could be maybe be due to the fact that LazyVGrid creates those on the fly when scrolling and might have some issues to order them consistently under hood. So what I did was just add .sorted modifier to the array in the ForEach loop. Seems to work for me now, but don'T know whether it finally solves the issue, but maybe worth a shot...
ScrollView {
VStack(alignment: .leading) {
VStack {
ForEach(Array(selectedSfSymbols.keys).sorted(by: { item1, item2 in
item1.rawValue < item2.rawValue
}), id: \.self) { cat in
VStack(alignment: .leading) {
Text(cat.rawValue)
.bold()
LazyVGrid(columns: columns, spacing: 20) {
ForEach(selectedSfSymbols[cat]!, id: \.self) { icon in
Button {
attributeIcon = icon
dismiss()
}
label: {
Label("", systemImage: icon)
.foregroundStyle(.pink)
.font(.title)
}
}
}
Divider()
.padding()
}
}
}
}
.padding()
.background(.ultraThinMaterial)
.cornerRadius(14)
}
.scrollIndicators(.never)
.padding()
} // END of scrollview
.navigationTitle("Select an Icon below")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button {
dismiss()
} label: {
Image(systemName: "xmark.circle")
.font(.title)
.foregroundStyle(Color.red)
}
}
} // END of toolbar
Upvotes: 0