SwiftUI Toggle multiple times per frame

I'm switching a remote Power Supply Unit on/off over the network. I need know if the PSU accepted the command, AND if at some future time it decides to switch itself off (or even on?). In other words I need feedback from the PSU to reflect it's current state via the very same Toggle that is sending the command. I hope that's all very clear.

Toggle("", isOn: $psu.isOn)
    .toggleStyle(SwitchToggleStyle(tint: Color.green))
    .onChange(of: psu.isOn, perform: { value in
        request(action: .PSU, param: psu.name)
    })

With my present code, I move the Toggle to on and the request asks the PSU to switch on. It sends back it's new status which informs the Toggle. The Toggle now thinks the status has changed, so it immediately toggles itself and sends a another request. At least I think that's what's happening.

The fact is, this message appears on the Xcode console:

onChange(of: Bool) action tried to update multiple times per frame

So, I'm hoping some kind person will be able to show me how to write better code that does what I really want. Thank you.

Answered by EA7K in 681947022

The workingdogintokyo solution does work. Unfortunately in my case, it would introduce too much bloatware that would be hard to maintain. There's little point in re-factoring only to then start adding loads of extra @Things all over the place So, I've decided to give up with those nice looking Toggles and just use plain old Buttons. They're not as elegant and they can't be customised as much as they can on iOS, but at least they work.

I have structs for various devices, but this is the one for the Power Supplies.

struct PowerSupplyControl: View {
    @Binding var psu: PowerSupplyStatus
    @EnvironmentObject var vm: ViewModel
    var body: some View {
        HStack {
            Text(psu.name)
            Spacer()
            Button(action: {
                vm.request(action: .PSU, param: psu.name)
            }, label: {
                Text(psu.isOn ? "ON" : "OFF")
                    .foregroundColor(psu.isOn ? .green : .primary)
            })
            .accentColor(.white)
            .frame(width: 40, alignment: .center)
            Text(String(format: "%05.2f V", psu.voltage)).frame(width: 50, alignment: .trailing)
            Text(String(format: "%05.2f A", psu.current)).frame(width: 50, alignment: .trailing)
        }
    }
}

Thanks again for all your time.

Your UI should reflect the state of the PSU.

You need to separate out:
• A user action sends a command to the PSU.
• The app displays the updated PSU state, whenever it is received.

Since the PSU can also be switched off (or even on?) outside of the app, you also need to take account of that.
That could be achieved (for example) by making the PSU an ObservableObject, and making isOn Published.

Here is a possible setup. I've used 2 states in a ObservableObject, one for the UI and one for the PSU. I change the UI only if there is a change.

import SwiftUI

@main
struct TestApp: App {
    @StateObject var psu = PSUModel()
    var body: some Scene {
        WindowGroup {
            ContentView().environmentObject(psu)
        }
    }
}

class PSUModel: ObservableObject {
    @Published var uiState = false
    @Published var psuState = false
}

struct ContentView: View {
    @EnvironmentObject var psu: PSUModel

    var body: some View {
        VStack (spacing: 50) {
            Button("simulate a PSU change") {
                psu.psuState.toggle()
            }
            Toggle("", isOn: $psu.uiState)
                .toggleStyle(SwitchToggleStyle(tint: Color.green))
                // changing the PSU state from the UI
                .onChange(of: psu.uiState) { value in
                    // request(action: .PSU, param: psu)
                    print("---> send to PSU: \(value)")
                    psu.psuState = value
                }
                // receiving PSU state changes from the PSU
                .onReceive(psu.$psuState) { value in
                    print("-----> received from PSU: \(value) ")
                    // update the UI with what we received from the PSU, but only if there is a change
                    if psu.uiState != value {
                        psu.uiState = value
                    }
                }
        }
    }
}
Accepted Answer

The workingdogintokyo solution does work. Unfortunately in my case, it would introduce too much bloatware that would be hard to maintain. There's little point in re-factoring only to then start adding loads of extra @Things all over the place So, I've decided to give up with those nice looking Toggles and just use plain old Buttons. They're not as elegant and they can't be customised as much as they can on iOS, but at least they work.

I have structs for various devices, but this is the one for the Power Supplies.

struct PowerSupplyControl: View {
    @Binding var psu: PowerSupplyStatus
    @EnvironmentObject var vm: ViewModel
    var body: some View {
        HStack {
            Text(psu.name)
            Spacer()
            Button(action: {
                vm.request(action: .PSU, param: psu.name)
            }, label: {
                Text(psu.isOn ? "ON" : "OFF")
                    .foregroundColor(psu.isOn ? .green : .primary)
            })
            .accentColor(.white)
            .frame(width: 40, alignment: .center)
            Text(String(format: "%05.2f V", psu.voltage)).frame(width: 50, alignment: .trailing)
            Text(String(format: "%05.2f A", psu.current)).frame(width: 50, alignment: .trailing)
        }
    }
}

Thanks again for all your time.

Have you tried wrapping request(action: .PSU, param: psu.name) in DispatchQueue.main.async in order to perform it on the next run loop?

SwiftUI Toggle multiple times per frame
 
 
Q