How do I disable a button while it has focus

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)
    }
}

Accepted Reply

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
        }
    }

Replies

I don't understand the logic here. When should ButtonDown200 be disabled (vm.pageUpDisabled200 true) ? Where is vm.pageUpDisabled25 used ?

            if (closingValuesCount - 1) - (vm.startIndex + 200) < 25 {
                vm.pageDownDisabled200 = true
            }
            if vm.startIndex > 24 {
                vm.pageUpDisabled25 = false
            }
            if vm.startIndex - 200 >= 0 {
                vm.pageUpDisabled200 = false
            }
        }

why not simply:

vm.pageDownDisabled200 = ((closingValuesCount - 1) - (vm.startIndex + 200) < 25) && (vm.startIndex< 200)

Closing value count variable holds the number of rows in the underlying data array. It is about 1200 rows. My table is set up to only show 25 rows from this array at a time. The 4 buttons, Page Up 200, Page Up 25, Page Down 25 and Page Down 200 allow the user to move up or down in the data. The start index variable holds the first row in the underlying array that will be displayed in the table. Upon initial display of the table, rows 0 thru 24 of the underlying array are displayed. If the user presses the Page Down 25 button the start index variable is now 25 and rows 25 thru 49 are displayed in the table. What I want to do is disable buttons that cannot do anything. For example if rows 0 thru 24 of the underlying array are being displayed then Page Up 25 and Page Up 200 should be disabled since there is no data above row 0 of the underlying array. In the logic I test to see if the start index variable is > 24. If so then the Page Up 25 button should no longer be disabled. Same for the Page Up 200 button. If the user has moved down through the underlying array and are now 200 or more rows from row 0 in the underlying array, then that button should no longer be disabled. In the initial if statement I am testing to see if I can no longer press the Page Down 200 button and fill the data table with 25 values, i.e. advancing 200 rows would place us closer to the last row in the underlying data array than 25 rows. In this case I want to disable the Page Down 200 button. I use the vm.pageDownDisabled200 (a boolean) to enable and disable the Page Down 200 button, which is a vew. If the value is true then the button should become disabled. It is used in the last line of the button view as .disabled(vm.pageDownDisabled200). This is where I seem to get into trouble. If the Page Down 200 button is enabled and has focus, trying to disable it appears to be the source of the error message "AttributeGraph: cycle detected through attribute 864480". But I could be wrong. Thank you for responding. Any insight you can provide will be appreciated.

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
        }
    }