listviewbuttonswiftuiscreen-sizevertical-scrolling

SwiftUI Why do my buttons on my list view stop working if I have too many items on the list view?


HELP! I can't figure this out!

To help me learn SwiftUI I am making a simple grocery list app using a list view. There are buttons that turn GREEN or RED when tapped for each item on the list.

The code below works great until I add too many items to my list array. If the list gets longer than the screen size and you have to scroll then weird things start happening. (like the button won't turn green or an item on the next page turns green instead)

I tested with different simulators and on an iPhone SE I can get 14 items on the list before it starts messing up. On an iPhone 11 Pro Max, I can get 22 items on the list before it starts messing up. It has something to do with the length of the screen size and if scrolling is required.

Here is the code:

    import SwiftUI

    struct CheckboxFieldView : View {
        @State var checkState:Bool = false ;
        var body: some View {
             Button(action:
                {
                    self.checkState = !self.checkState
            }) {
                       Rectangle()
                                .fill(self.checkState ? Color.green : Color.red)
                                .frame(width:20, height:20, alignment: .center)
                                .cornerRadius(5)
            }
            .foregroundColor(Color.white)
        }
    }

    struct ContentView: View {
    //    let items = (1...100).map { number in "Item \(number)" }

    let items = ["Bread", "Milk", "Cheese", "Granola", "Nuts", "Cereal", "Rosemary", "Tomato Sauce", "Bean with Bacon Soup", "Tea", "Chocolate Milk", "Frozen Blueberries", "Frozen Mixed Berries", "Frozen Strawberries", "Grapes"]

        var body: some View {
            NavigationView {
                List(items, id: \.self) { item in
                    HStack{
                        CheckboxFieldView()
                        Text(item)
                        self.padding()
                        Text ("Location")
                    }
                }.navigationBarTitle("Grocery List", displayMode: .inline)
            }
        }
    }

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Solution

  • Well, in this case it is an issue of modelling... The @State is view state property, so List having cached views, for optimisation & reuse purpose, preserves the view internal state itself. Thus in such scenarios state cannot be used for persistency purpose and it is required to have explicit view model, and actually it is true for all model cases.

    Here is modified demo code, using view model based on ObservableObject and @Binding for inter-view data pass, that works properly...

    import SwiftUI
    import Combine
    
    // model to keep groceries
    struct FoodItem: Identifiable {
        var id = UUID().uuidString
        var title: String
        var checked = false
    }
    
    // view model part 
    class FoodViewModel: ObservableObject {
        @Published var items = [FoodItem]()
    }
    
    struct CheckboxFieldView : View {
        @Binding var item: FoodItem // bind to represented model
        var body: some View {
             Button(action:
                {
                    self.item.checked.toggle()
            }) {
                       Rectangle()
                                .fill(self.item.checked ? Color.green : Color.red)
                                .frame(width:20, height:20, alignment: .center)
                                .cornerRadius(5)
            }
            .foregroundColor(Color.white)
        }
    }
    
    struct ContentView: View {
    
        @ObservedObject private var viewModel = FoodViewModel()
        let items = ["Bread", "Milk", "Cheese", "Granola", "Nuts", "Cereal", "Rosemary", "Tomato Sauce", "Bean with Bacon Soup", "Tea", "Chocolate Milk", "Frozen Blueberries", "Frozen Mixed Berries", "Frozen Strawberries", "Grapes"]
    
        init() {
            viewModel.items = items.map { FoodItem(title: $0) } // fill in demo model items
        }
    
        var body: some View {
            NavigationView {
                List(Array(viewModel.items.enumerated()), id: \.element.id) { (i, item) in
                    HStack{
                        CheckboxFieldView(item: self.$viewModel.items[i]) // bind to current item in view model
                        Text(item.title)
                        Spacer()
                        Text ("Location")
                    }
                }.navigationBarTitle("Grocery List", displayMode: .inline)
            }
        }
    }