swiftuiobservableobject

SwiftUI ObservableObject and Published issue


here is something that keeps me awake for three days already: I'm writing a little app that connects via BlueTooth to an Arduino. To get visual feedback about the connection state and the transmitted data, I use a view that allows me to connect/disconnect as well as shows me the state and data:

        VStack {
            Text("Glove Training App")
                .font(.title)
            HStack {
                Button(action: { MyBluetoothManager.shared.scan() }) {
                    Text("Connect")
                        .padding(30)
                }
                Text(" | ")
                Button(action: { MyBluetoothManager.shared.disconnect()}) {
                    Text("Disconnect")
                    .padding(30)
                }
            }

            Text(manager.stateChange)
                .font(.subheadline)
                .padding(.bottom, 30)
            
            Text(peripheral.transmittedString)
            .font(.subheadline)
            .padding(.bottom, 30)
        }
    }

In a separate file I have all the BT management:

class MyBluetoothManager: NSObject, ObservableObject {
    @Published var stateChange: String = "Initializing..." {
        willSet { objectWillChange.send() }
    }
    static let shared = MyBluetoothManager()
    
    let central = CBCentralManager(delegate: MyCentralManagerDelegate.shared,
        queue: nil, options: [
        CBCentralManagerOptionRestoreIdentifierKey: restoreIdKey,
        ])
(...)
    func setConnected(peripheral: CBPeripheral) {
        (...)
        
        state = .connected(peripheral)
        self.stateChange = "Connected"
        print("Connected")
    }
}

class MyPeripheralDelegate: NSObject, ObservableObject, CBPeripheralDelegate {
    let objectWillChange = ObservableObjectPublisher()
    var transmittedString: String = "No data" {
        willSet { objectWillChange.send()
        }
    }
    func peripheral(_ peripheral: CBPeripheral,
            didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
        (...)
        let rxData = characteristic.value
        if let str = NSString(data: rxData!, encoding: String.Encoding.utf8.rawValue) as String? {
            print(str)
            self.transmittedString = str
            let measurement = str.components(separatedBy: "|")
            (...)
        } else {
            print("not a valid UTF-8 sequence")
        }
    }
}

The values are initially set correctly, but then never updated. In the terminal I can see the printed values and the app works otherwise as expected. I'm on the latest version of XCode. I looked at several tutorials, and this seems to be tricky. Any help would be highly appreciated.

Cheers, Christian

EDIT: Here is the full BluetoothManager class (not my code mostly but works fine):

class MyBluetoothManager: NSObject, ObservableObject {
    @Published var stateChange: String = "Initializing..." {
        willSet { objectWillChange.send() }
    }
    static let shared = MyBluetoothManager()
    
    let central = CBCentralManager(delegate: MyCentralManagerDelegate.shared,
        queue: nil, options: [
        CBCentralManagerOptionRestoreIdentifierKey: restoreIdKey,
        ])
    
    var state = State.poweredOff
    enum State {
        case poweredOff
        case restoringConnectingPeripheral(CBPeripheral)
        case restoringConnectedPeripheral(CBPeripheral)
        case disconnected
        case scanning(Countdown)
        case connecting(CBPeripheral, Countdown)
        case discoveringServices(CBPeripheral, Countdown)
        case discoveringCharacteristics(CBPeripheral, Countdown)
        case connected(CBPeripheral)
        case outOfRange(CBPeripheral)
        
        var peripheral: CBPeripheral? {
            switch self {
            case .poweredOff: return nil
            case .restoringConnectingPeripheral(let p): return p
            case .restoringConnectedPeripheral(let p): return p
            case .disconnected: return nil
            case .scanning: return nil
            case .connecting(let p, _): return p
            case .discoveringServices(let p, _): return p
            case .discoveringCharacteristics(let p, _): return p
            case .connected(let p): return p
            case .outOfRange(let p): return p
            }
        }
    }
    
    func scan() {
        guard central.state == .poweredOn else {
            self.stateChange = "Cannot scan, BT is not powered on"
            print("Cannot scan, BT is not powered on")
            return
        }
        
        central.scanForPeripherals(withServices: [myDesiredServiceId], options: nil)
        state = .scanning(Countdown(seconds: 10, closure: {
            self.central.stopScan()
            self.state = .disconnected
            self.stateChange = "Scan timed out"
            print("Scan timed out")
            
        }))
    }
    
    func disconnect(forget: Bool = false) {
        if let peripheral = state.peripheral {
            central.cancelPeripheralConnection(peripheral)
        }
        if forget {
            UserDefaults.standard.removeObject(forKey: peripheralIdDefaultsKey)
            UserDefaults.standard.synchronize()
        }
        self.stateChange = "Disconnected"
        state = .disconnected
    }

    func connect(peripheral: CBPeripheral) {
        central.connect(peripheral, options: nil)
        state = .connecting(peripheral, Countdown(seconds: 10, closure: {
            self.central.cancelPeripheralConnection(peripheral)
            self.state = .disconnected
            self.stateChange = "Connect timed out"
            print("Connect timed out")
        }))
    }
    
    func discoverServices(peripheral: CBPeripheral) {
        peripheral.delegate = MyPeripheralDelegate.shared
        peripheral.discoverServices([myDesiredServiceId])
        state = .discoveringServices(peripheral, Countdown(seconds: 10, closure: {
            self.disconnect()
            self.stateChange = "Could not discover services"
            print("Could not discover services")
        }))
    }
    
    func discoverCharacteristics(peripheral: CBPeripheral) {
        guard let myDesiredService = peripheral.myDesiredService else {
            self.disconnect()
            return
        }
        peripheral.delegate = MyPeripheralDelegate.shared
        peripheral.discoverCharacteristics([myDesiredCharacteristicId],
            for: myDesiredService)
        state = .discoveringCharacteristics(peripheral, Countdown(seconds: 10,
            closure: {
            self.disconnect()
                self.stateChange = "Could not discover characteristics"
            print("Could not discover characteristics")
        }))
    }

    func setConnected(peripheral: CBPeripheral) {
        guard let myDesiredCharacteristic = peripheral.myDesiredCharacteristic
            else {
                self.stateChange = "Missing characteristic"
                print("Missing characteristic")
                disconnect()
            return
        }
        
        UserDefaults.standard.set(peripheral.identifier.uuidString,
            forKey: peripheralIdDefaultsKey)
        UserDefaults.standard.synchronize()

        peripheral.delegate = MyPeripheralDelegate.shared
        peripheral.setNotifyValue(true, for: myDesiredCharacteristic)
        
        state = .connected(peripheral)
        self.stateChange = "Connected"
        print("Connected")
    }
}

Solution

  •            Button(action: { MyBluetoothManager.shared.scan() }) {
                    Text("Connect")
                        .padding(30)
                }
                Text(" | ")
                Button(action: { MyBluetoothManager.shared.disconnect()}) {
                    Text("Disconnect")
                    .padding(30)
                }
            }
    
            Text(manager.stateChange) << why don't you use MyBluetoothManager.shared here ? is there a second instance? this might be the error...but unfortunately you just showed us a small piece of code...