import SwiftUI
// https://nilcoalescing.com/blog/
@available(iOS 18.0, *)
struct RecyclingScrollingLazyView<
    ID: Hashable, Content: View
>: View {
    let rowIDs: [ID]
    let rowHeight: CGFloat
    
    @ViewBuilder
    var content: (ID) -> Content
    
    var numberOfRows: Int { rowIDs.count }
    @State var visibleRange: Range<Int> = 0..<1
    @State var rowFragments: Int = 1
    
    struct RowData: Identifiable {
        let fragmentID: Int
        let index: Int
        let value: ID
        
        var id: Int { fragmentID }
    }
    var visibleRows: [RowData] {
        if rowIDs.isEmpty { return [] }
        
        let lowerBound = min(
            max(0, visibleRange.lowerBound),
            rowIDs.count - 1
        )
        let upperBound = max(
            min(rowIDs.count, visibleRange.upperBound),
            lowerBound + 1
        )
        
        let range = lowerBound..<upperBound
        let rowSlice = rowIDs[lowerBound..<upperBound]
        
        let rowData = zip(rowSlice, range).map { row in
            RowData(
                fragmentID: row.1 % max(rowFragments, range.count),
                index: row.1, value: row.0
            )
        }
        return rowData
    }
    
    var body: some View {
        ScrollView(.vertical) {
            OffsetLayout(
                totalRowCount: rowIDs.count,
                rowHeight: rowHeight
            ) {
                // The fragment ID is used instead of the row ID
                ForEach(visibleRows) { row in
                    content(row.value)
                        .layoutValue(
                            key: LayoutIndex.self, value: row.index
                        )
                }
            }
        }
        .onScrollGeometryChange(
            for: Range<Int>.self,
            of: { geo in
                self.computeVisibleRange(in: geo.visibleRect)
            },
            action: { oldValue, newValue in
                self.visibleRange = newValue
                self.rowFragments = max(
                    newValue.count, rowFragments
                )
            }
        )
    }
    
    func computeVisibleRange(in rect: CGRect) -> Range<Int> {
        let lowerBound = Int(
            max(0, floor(rect.minY / rowHeight))
        )
        let upperBound = max(
            Int(ceil(rect.maxY / rowHeight)),
            lowerBound + 1
        )
        return lowerBound..<upperBound
    }
}

struct OffsetLayout: Layout {
    let totalRowCount: Int
    let rowHeight: CGFloat
    
    func sizeThatFits(
        proposal: ProposedViewSize,
        subviews: Subviews,
        cache: inout ()
    ) -> CGSize {
        CGSize(
            width: proposal.width ?? 0,
            height: rowHeight * CGFloat(totalRowCount)
        )
    }
    
    func placeSubviews(
        in bounds: CGRect,
        proposal: ProposedViewSize,
        subviews: Subviews,
        cache: inout ()
    ) {
        for subview in subviews {
            let index = subview[LayoutIndex.self]
            subview.place(
                at: CGPoint(
                    x: bounds.midX,
                    y: bounds.minY + rowHeight * CGFloat(index)
                ),
                anchor: .top,
                proposal: .init(
                    width: proposal.width, height: rowHeight
                )
            )
        }
    }
}

struct LayoutIndex: LayoutValueKey {
    nonisolated(unsafe) static var defaultValue: Int = 0
    
    typealias Value = Int
}

struct MyRow: View {
    let id: Int
    @State private var text: String = ""
    
    var body: some View {
        TextField("Enter something", text: $text)
            .onChange(of: id) { old, newID in
                self.text = ""
            }
    }
}

#Preview {
    if #available(iOS 18.0, *) {
        
        RecyclingScrollingLazyView(
            rowIDs: Array(1...1000), rowHeight: 42
        ) { id in
            MyRow(id: id)
        }
    } else {
        // Fallback on earlier versions
    }
}