Rillieux
Rillieux

Reputation: 697

How can I get my Button to reflect the Bool of my EnvironmentObject?

I'm trying to use an @EnvironmentObject to track the Bool state of a few arrays of objects but I'm hitting an out of range Index and cannot figure out why.

I'm trying to track a "Query" that selects between three containers. Each container has a set numbers of "frames" and I need to toggle those frames and change the colour of the button label:

    import SwiftUI

    class Query: ObservableObject {
        
        @Published var selectedContainer: Int = 2
        @Published var frames: [Bool] = [true, true, true, true, true, true, true, true]
        
        func resetFrames() {
            switch selectedContainer {
                case 0:
                    self.frames = [true, true, true, true, true]
                    print("Boxes")
                case 1:
                    self.frames = [true, true, true]
                    print("Bags")
                case 2:
                    self.frames = [true, true, true, true, true, true, true, true]
                    print("Barrels")
                default:
                    self.frames = [true, true, true]
            }
        }
            
        func satisfiedFrames() {
            let shouldReset = frames.allSatisfy { $0 == false }
            print(shouldReset)
            if shouldReset == true {
                resetFrames()
            }
        }
     }

    enum Container {
        case box
        case bag
        case barrel
        
        var tag: Int {
            switch self {
                case .box:
                    return 0
                case .bag:
                    return 1
                case .barrel:
                    return 2
            }
        }
        
        var name: String {
            switch self {
                case .box:
                    return "Box"
                case .bag:
                    return "Bag"
                case .barrel:
                    return "Barrel"
            }
        }
    }

I have my colours in an array, too:

let dvColors: [Color] = [
    Color.red,
    Color.orange,
    Color.yellow,
    Color.green,
    Color.blue,
    Color.indigo,
    Color.purple,
    Color.pink
]

Then I have a picker to switch between the containers like this:

    @EnvironmentObject var query: Query
    var container: [Container] = [.box, .bag, .barrel]
    
    var body: some View {
        Picker(selection: $query.selectedContainer, label: Text("Container")){
            ForEach(0..<container.count) { index in
                Text(self.container[index].name)
                    .tag(index)
            }
        }
        .onChange(of: query.selectedContainer, perform: changeContainer)
    }
    
    func changeContainer(_ tag: Int) {
        print("CHANGE CONTAINER ON PICKER")
        print("TAG: \(tag)")
        query.selectedContainer = tag
        print("QUERY CONTAINER: \(query.selectedContainer)")
        query.resetFrames()
        print("FRAMES COUNT:  \(query.frames.count)")
    }
}

And finally, this is my content view:

import SwiftUI

struct ContentView: View {
    @StateObject var query = Query()
    var body: some View {
        NavigationView {
            ZStack {
                ScrollView {
                    Text("FRAME COUNT: \(query.frames.count)")
                    Text("Container: \(query.selectedContainer)")
                    Spacer()
                }
                .navigationBarTitleDisplayMode(.inline)
                .toolbar(){
                    ToolbarItem(placement: .principal, content: {
                        ContainerPicker()
                    })
                }
                VStack {
                    Spacer()
                    TheHStack()
                }
            }
        }
        .environmentObject(query)
    }
}

struct TheHStack: View {
    
    @EnvironmentObject var query: Query
    
    var body: some View {
        print(query.frames)
        return HStack (spacing: 10) {
            ForEach(query.frames.indices, id: \.self) { value in
                ColouredText(value: value)
            }
        }
    }
}

struct ColouredText: View {
    
    @EnvironmentObject var query: Query
    
    var value: Int
    
    var body: some View {
        
        Button(action: {
            query.frames[value].toggle()
            print(query.frames)
            print("\(value)")
        }, label: {
            Text("\(value + 1)")
                .foregroundColor(query.frames[value] ? dvColors[value] : .gray) // THIS LINE FAILS, I GUESS WHEN LOOKING UP THE COLOUR?
            
        })
    }
}

What I expect to do is when I make a selection with the picker, the Bools are all set to TRUE for the container. Then tapping buttons toggles frames in that container. And when selecting another container with the picker, the new container's frames are all TRUE. Also, if all the Bools go to FALSE, then they auto reset to all TRUE.

IF I change that colour ternary to simply one colour, everything works. And my print statements are showing the correct changes. But I can't get the UI to update correctly.

Screenshot

Upvotes: 1

Views: 215

Answers (1)

George
George

Reputation: 30341

Method 1

You can just do a quick check if there will be an out-of-bounds error. SwiftUI sometimes holds views for a little longer and it can cause issues like this.

In ColouredText, change:

.foregroundColor(query.frames[value] ? dvColors[value] : .gray)

To:

.foregroundColor(value < query.frames.count && query.frames[value] ? dvColors[value] : .gray)

Method 2

Another way to fix this (but I probably wouldn't do this, because you are taking away the splitting of views) is to just put ColouredText's body directly where it is needed:

struct TheHStack: View {
    @EnvironmentObject var query: Query

    var body: some View {
        print(query.frames)
        return HStack (spacing: 10) {
            ForEach(query.frames.indices, id: \.self) { value in
                Button(action: {
                    query.frames[value].toggle()
                    print(query.frames)
                    print("\(value)")
                }, label: {
                    Text("\(value + 1)")
                        .foregroundColor(query.frames[value] ? dvColors[value] : .gray)
                })
            }
        }
    }
}

Method 3

You can also fix this by passing in query at the same time as value. This means that they will never be out of sync.

ForEach(query.frames.indices, id: \.self) { value in
    ColouredText(value: value, query: query)
}
struct ColouredText: View {
    let value: Int
    let query: Query

    var body: some View {
        /* ... */
    }
}

Upvotes: 0

Related Questions