SwiftUI and Bluetooth

In this post, I show how to connect a Bluetooth device from SwiftUI and how to receive data.

The code used in this example is here https://github.com/niqt/Blue

Proceed for step: the things to do are: check if the Bluetooth is enabled, scan to search the devices, connect with one and wait for data. The check of the Bluetooth status and the connection is a feature of the connection manager, the others of the peripheral. Let’s see it in details.

Every device has a service, identified with a UUID and any service can have one more property. The properties can be read-only (to read value) or read-write (write is like send a command). Also, the properties are identified with a UUID.

Let’s look at the code:

import Foundation
import CoreBluetooth

Then define the peripherical struct:

struct Peripheral: Identifiable {
    let id: Int
    let name: String
    let rssi: Int
}

We use this struct to expose the peripherical to SwiftUI.

Now, start to look at the core of the application:

class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeripheralDelegate {
    var centralBE: CBCentralManager!
    @Published var isSwitchedOn = false
    @Published var peripherals = [Peripheral]()
    var peripheralsId = [UUID]()
    var thermometer: CBPeripheral!
    var serviceId = CBUUID(string: "00000001-710e-4a5b-8d75-3e5b444b3c3f")
    var readNotify = CBUUID(string: "00000002-710e-4a5b-8d75-3e5b444b3c3f")
    var readWrite = CBUUID(string: "00000003-710e-4a5b-8d75-3e5b444b3c3f")

The BLEManger is an NSObject that is observable (from the SwiftUI side) and implements the protocols to manage the Bluetooth status and services. Instead, the CBPeripheralDelegate protocol it’s about peripherical and properties operation.

So, the CBCentralManager is the Bluetooth manager. The isSwitchedOn contains the status of the Bluetooth and the peripherals array contains the list of the peripherals. These last two variables are published, so the value can be get from SwiftUI.

The thermometer peripheral contains the data of the peripheral on that we want to work in this example, the temperature of a RaspberryPI (the code is here https://github.com/Douglas6/cputemp). Thus, with our app, we want to read the CPU temp of a raspberry. The serviceId and properties are from this Raspberry example, you are free to replace them with anything you want.

With the init, simply initialize the manager and the delegate:

override init() {
        super.init()
        centralBE = CBCentralManager(delegate: self, queue: nil)
        centralBE.delegate = self
}

This function is called when the status changes:

func centralManagerDidUpdateState(_ central: CBCentralManager) {
        if central.state == .poweredOn {
            isSwitchedOn = true
        }
        else {
            isSwitchedOn = false
        }
}

If the state is .poweredOn (the Bluetooth is enabled on the device) we can start the scanning:

func startScanning() {
        print("startScanning")
        centralBE.scanForPeripherals(withServices: nil, options: nil)
}

After the scanning is started, with this function it’s possible to see the device around us:

func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        var peripheralName: String!
        
        if let name = advertisementData[CBAdvertisementDataLocalNameKey] as? String {
            peripheralName = name
            let newPeripheral = Peripheral(id: peripherals.count, name: peripheralName, rssi: RSSI.intValue)
            if !peripheralsId.contains(peripheral.identifier) && peripheralName == "Thermometer" {
                peripheralsId.append(peripheral.identifier)
                peripherals.append(newPeripheral)
                stopScanning()
                self.thermometer = peripheral
                self.thermometer.delegate = self
                self.centralBE.connect(peripheral, options: nil)
            }
        }
        else {
            peripheralName = "Unknown"
        }
    }

If the Thermometer peripheral is discovered, it’s added the the array of peripheral, the scanning is stopped, set the delegate for the peripheral to the self and to simplify connect to device (without user interaction).

If the connection has success, this function is called:

 func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        print("DidConnect")
        discoverServices(peripheral: peripheral)
    }

Thus starts to discover the services:

func discoverServices(peripheral: CBPeripheral) {
        peripheral.discoverServices([serviceId])
    }

If at least one service is discovered, start to search characteristics:

func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        guard let services = peripheral.services else {
            print("ERROR didDiscoverServices")
            return
        }
        if services.count > 0 {
            discoverCharacteristics(peripheral: peripheral)
        }
    }

When the characteristic is found:

func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        service.characteristics?.forEach { char in
            print("CHAR id: \(char.uuid.uuidString) VALUE: \(String(describing: char.value) )")
            peripheral.readValue(for: char)
            
        }
    }

In the BLEManger.swift file you can see also other two lines functions, their name are. self-explanatory.

Take a look to the SwiftUI code:

struct ContentView: View {
    @ObservedObject var bleManager = BLEManager()
    @State private var scan = false
    
    var body: some View {
        VStack (spacing: 10) {
            Text("STATUS")
                .font(.headline)
            
            if bleManager.isSwitchedOn {
                Text("ON")
                    .foregroundColor(.green)
            }
            else {
                Text("OFF")
                    .foregroundColor(.red)
            }
            HStack {
                Spacer()
                Toggle("Scan", isOn: $scan)
                    .toggleStyle(SwitchToggleStyle(tint: .red))
                    .onChange(of: scan) { value in
                        // action...
                        self.bleManager.startScanning()
                    }
                
            }
            Text("Bluetooth Devices")
                .font(.largeTitle)
                .frame(maxWidth: .infinity, alignment: .center)
            List(bleManager.peripherals) { peripheral in
                HStack {
                    Text(peripheral.name)
                    Spacer()
                    Text(String(peripheral.rssi))
                }
            }.frame(height: 300)
            Spacer()
        }
    }
}

We can define an observable object, the BLEManager, so we can get the Bluetooth status, with the toggle button active the scan and in the end, show the peripheral list in a SwiftUI list. Enjoy with your peripheral!

The result

Note: English is not my native language, so I’m sorry for some errors. I appreciate it if your correct me.

share this post with friends

Picture of Nicola De filippo

Nicola De filippo

I'm a software engineer who adds to the passion for technologies the wisdom and the experience without losing the wonder for the world. I love to create new projects and to help people and teams to improve

2 comments

Leave a comment

Your email address will not be published. Required fields are marked *

Who I am

I'm a software engineer who adds to the passion for technologies the wisdom and the experience without losing the wonder for the world. I love to create new projects and to help people and teams to improve.

Follow Me Here

Get The Latest Updates

Periodically receive my super contents on coding and programming

join the family;)

Recent Posts