Reputation: 697
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.
Upvotes: 1
Views: 215
Reputation: 30341
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)
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)
})
}
}
}
}
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