SwiftUI List with Section and Swipe Actions loads all rows at once

Viewed 219

Consider the following code example (you can download it here):

struct Item: Identifiable {
    var id = UUID()
    var name: String
}

struct Row: View {

    var item: Item
    static var counter = 0

    init(item: Item) {
        self.item = item

        Row.counter += 1
        print(Row.counter)
    }

    var body: some View {
        Text(item.name)
    }
}

struct ContentView: View {

    @State var items = (1...1000).map { Item(name: "Item \($0)") }

    var body: some View {
        List {
            ForEach(items) {
                Row(item: $0)
                    .swipeActions(edge: .leading) {
                        Button("Action", action: {})
                    }
            }
        }
    }
}

Running this code prints out the numbers 1 to 21, so around the amount of rows that are visible on the screen.

Now if I wrap the ForEach statement in a Section, the numbers 1 to 1000 are printed out. Hence, there is no cell reuse and all rows are loaded at once.

Section {
    ForEach(items) {
        Row(item: $0)
            .swipeActions(edge: .leading) {
                Button("Action", action: {})
            }
    }
}

If I remove the swipe action, the numbers 1 to 18 are printed out.

Section {
    ForEach(items) {
        Row(item: $0)
    }
}

Is this a known issue or what am I doing wrong here?

1 Answers

I also encountered this issue even without the swipe action. I create a new SectionView for Lists with .insetGroup style until Apple fixes it. The downside is that the section header and footer are not equal to the original view.

It was tested and is working with iOS 15 and 16.

struct LazySection<Element, Row: View>: View where Element: Equatable, Element: Identifiable {

    // source: https://medium.com/devtechie/round-specific-corners-in-swiftui-d23ceee08188
    struct RoundedCorner: Shape {
        var radius: CGFloat = .infinity
        var corners: UIRectCorner = .allCorners

        func path(in rect: CGRect) -> Path {
            let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
            return Path(path.cgPath)
        }
    }

    let elements: [Element]
    var header: String?
    var footer: String?
    @ViewBuilder let row: (_ element: Element) -> Row

    var body: some View {

        if let header = header {
            HeaderFooter(header, isHeader: true)
                .textCase(.uppercase)
        }

        if let first = elements.first,
           let last = elements.last {

            if first == last {
                row(first)
                    .listRowBackground(
                        Rectangle()
                            .foregroundColor(.white)
                            .clipShape(RoundedCorner(radius: 10))
                    )
            } else {
                row(first)
                    .listRowBackground(
                        Rectangle()
                            .foregroundColor(.white)
                            .clipShape(RoundedCorner(radius: 10, corners: [.topLeft, .topRight]))
                    )
                ForEach(elements.dropFirst().dropLast()) { element in
                    row(element)
                }
                row(last)
                    .listRowBackground(
                        Rectangle()
                            .foregroundColor(.white)
                            .clipShape(RoundedCorner(radius: 10, corners: [.bottomLeft, .bottomRight]))
                    )
            }
        }

        if let footer = footer {
            HeaderFooter(footer, isHeader: false)
        }
    }

    func HeaderFooter(_ title: String, isHeader: Bool) -> some View {
        VStack(alignment: .leading) {
            if isHeader == true {
                Spacer()
            }
            Text(title)
                .font(.footnote)
            if isHeader == false {
                Spacer()
            }
        }
        .foregroundColor(.init(uiColor: .secondaryLabel))
        .listRowBackground(Color(uiColor: .systemGroupedBackground))
        .listRowSeparator(.hidden)
    }
}

Using it with your given code example Item must conform to Equatable.

struct Item: Identifiable, Equatable {
    var id = UUID()
    var name: String
}

struct Row: View {

    var item: Item
    static var counter = 0

    init(item: Item) {
        self.item = item

        Row.counter += 1
        print(Row.counter)
    }

    var body: some View {
        Text(item.name)
    }
}

struct ContentView: View {

    @State var items = (1...1000).map { Item(name: "Item \($0)") }

    var body: some View {
        List {
            LazySection(elements: items) { element in
                Row(item: element)
            }
        }
    }
}
Related