swiftuilazyvgrid

How can I change button status in SwiftUI?


I am trying to change the color programmatically of some buttons in SwiftUI. The buttons are stored in a LazyVGrid. Each button is built via another view (ButtonCell). I'm using a @State in the ButtonCell view to check the button state. If I click on the single button, his own state changes correctly, just modifying the @State var of the ButtonCell view. If I try to do the same from the ContentView nothing is happening.

This is my whole ContentView (and ButtonCell) view struct:

struct ContentView: View {
    
    private var gridItemLayout = [GridItem(.adaptive(minimum: 30))]
    
    var body: some View {
        let columns = [
            GridItem(.flexible()),
            GridItem(.flexible()),
            GridItem(.flexible()),
            GridItem(.flexible()),
            GridItem(.flexible())
        ]
        ScrollView {
            LazyVGrid(columns: columns, spacing: 0) {
                ForEach(0..<10) { number in
                    ButtonCell(value: number + 1)
                }
            }
        }
        Button(action: {
            ButtonCell(value: 0, isEnabled: true)
            ButtonCell(value: 1, isEnabled: true)
            ButtonCell(value: 1, isEnabled: true)
            
        }){
            Rectangle()
                .frame(width: 200, height: 50)
                .cornerRadius(10)
                .shadow(color: .black, radius: 3, x: 1, y: 1)
                .padding()
                .overlay(
                    Text("Change isEnabled state").foregroundColor(.white)
                )
        }
        
    }
    
    
    struct ButtonCell: View {
        var value: Int
        @State var isEnabled:Bool = false
        
        var body: some View {
            Button(action: {
                print (value)
                print (isEnabled)
                isEnabled = true
                
            }) {
                Rectangle()
                    .foregroundColor(isEnabled ? Color.red : Color.yellow)
                    .frame(width: 50, height: 50)
                    .cornerRadius(10)
                    .shadow(color: .black, radius: 3, x: 1, y: 1)
                    .padding()
                    .overlay(
                        Text("\(value)").foregroundColor(.white)
                    )
            }
        }
        
    }
}

How I may change the color of a button in the LazyVGrid by clicking the "Change isEnabled state" button?


Solution

  • You need a different approach here. Currently you try to change the State of ButtonCell from the outside. State variables should always be private and therefore should not be changed from outside. You should swap the state and parameters of ButtonCell into a ViewModel. The ViewModels then are stored in the parent View (ContentView) and then you can change the ViewModels and the child views automatically update. Here is a example for a ViewModel:

    final class ButtonCellViewModel: ObservableObject {
        @Published var isEnabled: Bool = false
        let value: Int
        
        init(value: Int) {
            self.value = value
        }
    }
    

    Then store the ViewModels in the ContentView:

    struct ContentView: View {
        let buttonViewModels = [ButtonCellViewModel(value: 0), ButtonCellViewModel(value: 1), ButtonCellViewModel(value: 2)]
        
        private var gridItemLayout = [GridItem(.adaptive(minimum: 30))]
        
        var body: some View {
            let columns = [
                GridItem(.flexible()),
                GridItem(.flexible()),
                GridItem(.flexible()),
                GridItem(.flexible()),
                GridItem(.flexible())
            ]
            ScrollView {
                LazyVGrid(columns: columns, spacing: 0) {
                    ForEach(0..<3) { index in
                        ButtonCell(viewModel: buttonViewModels[index])
                    }
                }
            }
            Button(action: {
                buttonViewModels[0].isEnabled.toggle()
            }){
                Rectangle()
                    .frame(width: 200, height: 50)
                    .cornerRadius(10)
                    .shadow(color: .black, radius: 3, x: 1, y: 1)
                    .padding()
                    .overlay(
                        Text("Change isEnabled state").foregroundColor(.white)
                    )
            }
        }
    }
    

    And implement the ObservedObject approach in ButtonCell.

    struct ButtonCell: View {
        @ObservedObject var viewModel: ButtonCellViewModel
        
        var body: some View {
            Button(action: {
                print (viewModel.value)
                print (viewModel.isEnabled)
                viewModel.isEnabled = true
                
            }) {
                Rectangle()
                    .foregroundColor(viewModel.isEnabled ? Color.red : Color.yellow)
                    .frame(width: 50, height: 50)
                    .cornerRadius(10)
                    .shadow(color: .black, radius: 3, x: 1, y: 1)
                    .padding()
                    .overlay(
                        Text("\(viewModel.value)").foregroundColor(.white)
                    )
            }
        }
    }