The goal of my app is to create a vertical list of circles (CircleView) that can be dynamically added and removed as they move in and out of the visible area of the screen.
The list is managed by a HelperView, which adds circles when they scroll into view and removes them when they scroll off-screen. I am using a custom PreferenceKey to track the position of each circle and decide when to add or remove items.
The issue arises when pressing the "Down" button, causing the following error: "Bound preference CGRectPreferenceKey tried to update multiple times per frame."
This happens because I am using the GeometryReader inside CircleView to track the position of each item in the HelperView. The problem occurs when multiple updates to the PreferenceKey happen within the same frame, which can result in conflicting updates to the view's state. This is likely due to the addItem and removeItem calls being triggered too frequently within the same frame while handling user input (like pressing the "Down" button).
Can you help by examining the addItem and removeItem logic in CircleView? It might help to optimize the checks and ensure that the preference updates happen only once per frame.
Here is the photo of my goal, the items inside blue rect is what I need to see as items.
import SwiftUI
struct ContentView: View {
private let itemsCount: Int = 40
private let name: String = "CustomLazyVStack"
@State private var yOffset: CGFloat = .zero
var body: some View {
VStack {
Spacer()
GeometryReader { geo in
Text(String(describing: geo.frame(in: .named(name))))
let maxHeight: CGFloat = geo.size.height
HelperView(itemsCount: itemsCount, maxHeight: maxHeight, name: name)
.offset(y: yOffset)
}
.coordinateSpace(name: name)
.background(Color.black.opacity(0.5))
.frame(height: 500.0)
Spacer()
HStack {
Button("UP") {
yOffset += 50.0
}
Button("Down") {
yOffset -= 50.0
}
}
}
.padding()
}
}
struct HelperView: View {
let itemsCount: Int
let maxHeight: CGFloat
let name: String
@State private var lazyItems: [ItemType] = [ItemType]()
var body: some View {
VStack(spacing: 0.0) {
ForEach(Array(lazyItems.enumerated()), id: \.element.id) { index, item in
CircleView(item: item, maxHeight: maxHeight, name: name,
addAction: { addItem(index: index + 1) },
removeAction: { removeItem(id: item.id) })
}
}
.onAppear {
if !lazyItems.contains(where: { value in (value.value == 0) }) {
lazyItems.append(ItemType(value: 0))
}
}
}
func addItem(index: Int) {
guard index < itemsCount else { return }
if !lazyItems.contains(where: { value in (value.value == index) }) {
lazyItems.append(ItemType(value: index))
}
}
func removeItem(id: UUID) {
lazyItems.removeAll(where: { value in value.id == id })
}
}
struct CircleView: View, Equatable {
let item: ItemType
let maxHeight: CGFloat
let name: String
let addAction: () -> Void
let removeAction: () -> Void
var body: some View {
// print("CircleView called for: " + String(describing: item.value))
return Color.clear
.frame(height: 50.0)
.overlay(content: {
Circle()
.fill(Color.red)
.frame(width: 50.0, height: 50.0)
.overlay(Circle().stroke(lineWidth: 1.0))
.overlay(Text("\(item.value)").foregroundStyle(.white))
.background(
GeometryReader { geo in
Color.clear
.preference(key: CGRectPreferenceKey.self, value: [item.id : geo.frame(in: .named(name))])
.onPreferenceChange(CGRectPreferenceKey.self) { newValue in
if let unwrappedRect: CGRect = newValue[item.id] {
// Issue is here: Bound preference CGRectPreferenceKey tried to update multiple times per frame.
if unwrappedRect.origin.y <= -unwrappedRect.size.height {
// print("remove", item.value, unwrappedRect.origin.y)
// removeAction()
}
if (unwrappedRect.origin.y + unwrappedRect.size.height) <= maxHeight {
// print(String(describing: item.value), ":" ,unwrappedRect.origin.y + unwrappedRect.size.height)
addAction()
}
}
}
}
)
})
}
static func == (lhs: Self, rhs: Self) -> Bool {
(lhs.item == rhs.item)
}
}
struct ItemType: Identifiable, Equatable {
let id: UUID = UUID()
let value: Int
static func == (lhs: Self, rhs: Self) -> Bool {
(lhs.id == rhs.id) && (lhs.value == rhs.value)
}
}
struct CGRectPreferenceKey: PreferenceKey {
typealias DictionaryType = [UUID: CGRect]
static var defaultValue: DictionaryType {
get {
return DictionaryType()
}
}
static func reduce(value: inout DictionaryType, nextValue: () -> DictionaryType) {
let nextValues: DictionaryType = nextValue()
for (itemKey, itemValue) in nextValues {
value[itemKey] = itemValue
}
}
}