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()
}
}
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)
}
}
}