Manuel
Manuel

Reputation: 15072

Accessing State's value outside of being installed on a View

At line #1 in the code below the compiler throws this warning at build time:

Accessing State's value outside of being installed on a View. This will result in a constant Binding of the initial value and will not update.

import SwiftUI

struct MyView: View {

    enum Status {
        case a, b
    }

    struct MyButton: Identifiable {
        let id = UUID()
        @State var status: Status = .a
        let action: (Binding<Status>)->Void
    }

    @State var buttons: [MyButton] = [
        MyView.MyButton(action: { status in
            status.wrappedValue = .b
        })
    ]
    
    var body: some View {
        VStack {
            ForEach(buttons) { button in
                Button("x") {
                    button.action(button.$status) // #1
                }
                .disabled(button.status == .b)
            }
        }
    }
}

struct MyView_Previews: PreviewProvider {
    static var previews: some View {
        MyView()
    }
}

When running the code, the button indeed does not become disabled after tapping on it. It seems that's what the error above describes.

There are existing questions and answers on SO regarding this error, like:

But they all seem to use classes in the code in question. Given that the code above uses only structs, is there a way to fix it without adding a class and @ObservableObject? The basic concept of the code should not change, i.e. from within the button action it should be possible to set the disabled state of the button.

Upvotes: 1

Views: 4827

Answers (1)

@State should only be used/declared inside a view. You could try this approach to disable the Button after tapping it and without adding a class.

struct MyView: View {

    enum Status {
        case a, b
    }

    struct MyButton: Identifiable {
        let id = UUID()
        var status: Status = .a  // <-- here
        let action: (Binding<Status>)->Void
    }

    @State var buttons: [MyButton] = [
        MyView.MyButton(action: { status in
            status.wrappedValue = .b
        })
    ]
    
    var body: some View {
        VStack {
            ForEach($buttons) { $button in  // <-- here
                Button("button x") {
                    button.action($button.status) // <-- here
                }
                .disabled(button.status == .b)
            }
        }
    }
}

EDIT-1:

@State var status is declared inside a custom struct MyButton (not inside a View), @State is only for use in Views, that is the reason for not using it there.

... why do we need to pass the 2-way binding to action? because that is what you said you wanted, by declaring let action: (Binding<Status>)->Void.

You could easily do without the let action: ..., to make all this work, see the code example below:

struct MyView: View {

    enum Status {
        case a, b
    }

    struct MyButton: Identifiable {
        let id = UUID()
        var status: Status = .a
    }
    
    @State var buttons: [MyButton] = [MyView.MyButton(), MyView.MyButton()]
    
    var body: some View {
        VStack {
            ForEach($buttons) { $button in  // <-- here
                Button("button \(button.id.uuidString)") {
                    button.status = .b // <-- here
                }
                .buttonStyle(.bordered)
                .disabled(button.status == .b)
                .foregroundColor(button.status == .b ? .red : .blue) // <-- for testing
            }
        }
    }
}

Upvotes: 3

Related Questions