skip to Main Content

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?

3

Answers


  1. 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(Color(uiColor: .secondarySystemGroupedBackground))
                                .clipShape(RoundedCorner(radius: 10))
                        )
                } else {
                    row(first)
                        .listRowBackground(
                            Rectangle()
                                .foregroundColor(Color(uiColor: .secondarySystemGroupedBackground))
                                .clipShape(RoundedCorner(radius: 10, corners: [.topLeft, .topRight]))
                        )
                    ForEach(elements.dropFirst().dropLast()) { element in
                        row(element)
                    }
                    row(last)
                        .listRowBackground(
                            Rectangle()
                                .foregroundColor(Color(uiColor: .secondarySystemGroupedBackground))
                                .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)
                }
            }
        }
    }
    
    Login or Signup to reply.
  2. Concept:

    • Don’t worry about creation of Row instances, it is just a struct (stack allocation), it is not expensive.
    • What is important is how many views are actually rendered, add a onAppear { }.
    • Only 18 are printed for iPhone 14 pro (height)

    Code:

    struct ContentView: View {
    
        @State var items = (1...1000).map { Item(name: "Item ($0)") }
    
        var body: some View {
            List {
                Section {
                    ForEach(items) { item in
                        Row(item: item)
                            .swipeActions(edge: .leading) {
                                Button("Action", action: {})
                            }
                            .onAppear {
                                print("name = (item.name)")
                            }
                    }
                }
            }
        }
    }
    

    Output

    name = Item 18
    name = Item 17
    name = Item 16
    name = Item 15
    name = Item 14
    name = Item 13
    name = Item 12
    name = Item 11
    name = Item 10
    name = Item 9
    name = Item 8
    name = Item 7
    name = Item 6
    name = Item 5
    name = Item 4
    name = Item 3
    name = Item 2
    name = Item 1
    
    Login or Signup to reply.
  3. As a workaround, you can createa LazyVStack with a ForEach loop and style it as a List. Of course in this case you’ll need to implement your own logic for deleting items. You can refer to this project where I replicated the Apple’s List using a LazyVStack
    https://github.com/MrRonanX/Test-Task.-List-Swipe-Actions

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search