I have a view that shows a table and 4 buttons. Each button allows the user to step forward and backwards through the data. Buttons are enabled and disabled based on where you are in the data. If you are less than 200 values to the end of the data for example, the "Page Down - 200" button is disabled. Everything works fine if the mouse is used to run the code associated with each button. But in this case I think the buttons never get focus. Focus I think remains with the sidebar content item that brought up the table view (am using a navigation split view). If I tab over to the page down 200 button and use the space bar to run its associated code I get the error "AttributeGraph: cycle detected through attribute 864480". I think the problem lies with attempting to disable the button while it has focus but am not 100% sure. I have tried to change the focus prior to disabling the button but I get the same error. I think there is some fundamental that I am missing. Below is my table view along with the page down 200 button view.
struct DataTable: View {
@FocusState var buttonWithFocus: Field?
@ObservedObject var vm: ButtonsViewModel = ButtonsViewModel.shared
@State private var hasAppeared = false
var closingValues: [TradingDayClose]
var heading: String = ""
init(fundName: String, closingValues: [TradingDayClose]) {
self.heading = fundName
self.closingValues = closingValues
}
var body: some View {
HStack {
Spacer()
.frame(width: 150)
GroupBox(heading) {
if hasAppeared {
Table (of: TradingDayClose.self) {
TableColumn("") { closingValue in
Text(dateToStringFormatter.string(from: closingValue.timeStamp!))
.id(closingValue.timeStamp!)
.textFormatting(fontSize: 14)
.frame(width: 100, alignment: .center)
} // end table column
TableColumn("") { closingValue in
Text(String(format: "$ %.2f", closingValue.close))
.textFormatting(fontSize: 14)
.frame(width: 100, alignment: .center)
} // end table column
} rows: {
ForEach((closingValues.indices), id: \.self) { index in
if vm.showData[index] == true {
TableRow(closingValues[index])
}
}
}
.focusable(false)
.frame(width: 250)
.overlay {
let tempValue1: String = "Date"
let tempValue2: String = "Closing Value"
Text(tempValue1).position(x: 63, y: 15)
.textFormatting(fontSize: 16)
Text(tempValue2).position(x: 180, y: 15)
.textFormatting(fontSize: 16)
}
} // end has appeared
} // end group box
.groupBoxStyle(Table2GroupBoxStyle())
Spacer()
.frame(width: 50)
VStack {
Spacer()
.frame(height: 100)
Form {
Section {
ButtonUp25(closingValuesCount: closingValues.count)
.focused($buttonWithFocus, equals: .btnUp25)
ButtonUp200(closingValuesCount: closingValues.count)
.focused($buttonWithFocus, equals: .btnUp200)
} header: {
Text("Page Up")
}
Spacer()
.frame(height: 20)
Section {
ButtonDown25(closingValuesCount: closingValues.count)
.focused($buttonWithFocus, equals: .btnDn25)
ButtonDown200(buttonWithFocus: $buttonWithFocus, closingValuesCount: closingValues.count)
.focused($buttonWithFocus, equals: .btnDn200)
} header: {
Text("Page Down")
}
} // end form
.navigationTitle("Data Table")
Spacer()
.frame(height: 100)
} // end v stack
Spacer()
.frame(width: 50)
} // end h stack
.onAppear {
vm.InitializeShowData(closingValueCount: closingValues.count)
vm.InitializeIndexes()
vm.InitializeButtons()
for i in vm.startIndex...vm.endIndex {
DispatchQueue.main.asyncAfter(deadline: .now() + vm.renderRate * Double(i)) {
vm.showData[i] = true
} // end dispatch queue main async
}
hasAppeared = true
}
} // end body
} // end struct
struct ButtonDown200: View {
var buttonWithFocus: FocusState<Field?>.Binding
@ObservedObject var vm: ButtonsViewModel = ButtonsViewModel.shared
var closingValuesCount: Int
var body: some View {
Button("Page Down - 200") {
for i in vm.startIndex...vm.endIndex {
vm.showData[i] = false
}
vm.startIndex = vm.startIndex + 200
vm.endIndex = vm.startIndex + 24
var j: Int = 0
for i in vm.startIndex...vm.endIndex {
DispatchQueue.main.asyncAfter(deadline: .now() + vm.renderRate * Double(j)) {
vm.showData[i] = true
}
j = j + 1
}
if (closingValuesCount - 1) - (vm.startIndex + 200) < 25 {
// buttonWithFocus.wrappedValue = .btnDn25
vm.pageDownDisabled200 = true
}
if vm.startIndex > 24 {
vm.pageUpDisabled25 = false
}
if vm.startIndex - 200 >= 0 {
vm.pageUpDisabled200 = false
}
}
.controlSize(.large)
.buttonStyle(.borderedProminent)
.disabled(vm.pageDownDisabled200)
}
}
After some additional testing I am convinced that the error message is a result of disabling a button while it has focus. So I added a state variable of type boolean and toggle it in every button's action. Then I watch the state variable with an .onChange in the DataTable view. In the .onChange I test to see if a button should no longer be enabled and set the focus to a button that should still be enabled. At the end of this .onChange I toggle a second state variable of type boolean. Then I watch this state variable with a second .onChange. In the second .onChange I have logic that disables the appropriate buttons based on where the user is in the data being displayed. Doing it this way assures that no button that is being disabled has focus. Below are the 2 on change sections of code.
.onChange(of: updateFocus, perform: { newValue in
switch buttonWithFocus {
case .btnUp200:
if vm.startIndex - 200 <= 0 {
buttonWithFocus = .btnUp25
}
case .btnUp25:
if vm.startIndex == 0 {
buttonWithFocus = .btnDn25
}
case .btnDn25:
if vm.endIndex == closingValues.count - 1 {
buttonWithFocus = .btnUp25
}
case .btnDn200:
if (closingValues.count - 1) - (vm.startIndex + 200) < 25 {
buttonWithFocus = .btnDn25
}
case .none:
print("no button has focus")
}
updateEnabledButtons.toggle()
})
.onChange(of: updateEnabledButtons) { newValue in
if vm.startIndex + 25 <= closingValues.count - 1 {
vm.pageDownDisabled25 = false
} else {
vm.pageDownDisabled25 = true
}
if (closingValues.count - 1) - (vm.startIndex + 200) >= 25 {
vm.pageDownDisabled200 = false
} else {
vm.pageDownDisabled200 = true
}
if vm.startIndex > 0 {
vm.pageUpDisabled25 = false
} else {
vm.pageUpDisabled25 = true
}
if vm.startIndex - 200 >= 0 {
vm.pageUpDisabled200 = false
} else {
vm.pageUpDisabled200 = true
}
}