Connect and communicate with Bluetooth devices using modern programming paradigms.
The Spezi Bluetooth module provides a convenient way to handle state management with a Bluetooth device, retrieve data from different services and characteristics, and write data to a combination of services and characteristics.
This package uses Apples CoreBluetooth framework under the hood.
Note
You will need a basic understanding of the Bluetooth Terminology and the underlying software model to understand the structure and API of the Spezi Bluetooth module. You can find a good overview in the Wikipedia Bluetooth Low Energy (LE) Software Model section or the Developer’s Guide to Bluetooth Technology.
You need to add the Spezi Bluetooth Swift package to your app in Xcode or Swift package.
Important
If your application is not yet configured to use Spezi, follow the Spezi setup article to set up the core Spezi infrastructure.
The Bluetooth
module needs to be registered in a Spezi-based application using the
configuration
in a
SpeziAppDelegate
:
class ExampleAppDelegate: SpeziAppDelegate {
override var configuration: Configuration {
Configuration {
Bluetooth {
// discover devices ...
}
}
}
}
Note
You can learn more about a Module
in the Spezi documentation.
The Bluetooth
module allows to declarative define your Bluetooth device using a BluetoothDevice
implementation and property wrappers
like Service
and Characteristic
.
The below code examples demonstrate how you can implement your own Bluetooth device.
First of all we define our Bluetooth service by implementing a BluetoothService
.
We use the Characteristic
property wrapper to declare its characteristics.
Note that the value types needs to be optional and conform to
ByteEncodable
,
ByteDecodable
or
ByteCodable
respectively.
struct DeviceInformationService: BluetoothService {
static let id: BTUUID = "180A"
@Characteristic(id: "2A29")
var manufacturer: String?
@Characteristic(id: "2A26")
var firmwareRevision: String?
}
We can use this Bluetooth service now in the MyDevice
implementation as follows.
Tip
We use the DeviceState
and DeviceAction
property wrappers to get access to the device state and its actions. Those two
property wrappers can also be used within a BluetoothService
type.
class MyDevice: BluetoothDevice {
@DeviceState(\.id)
var id: UUID
@DeviceState(\.name)
var name: String?
@DeviceState(\.state)
var state: PeripheralState
@Service var deviceInformation = DeviceInformationService()
@DeviceAction(\.connect)
var connect
@DeviceAction(\.disconnect)
var disconnect
required init() {}
}
We use the above BluetoothDevice
implementation to configure the Bluetooth
module within the
SpeziAppDelegate.
import Spezi
class ExampleDelegate: SpeziAppDelegate {
override var configuration: Configuration {
Configuration {
Bluetooth {
// Define which devices type to discover by what criteria .
// In this case we search for some custom FFF0 service that is advertised.
Discover(MyDevice.self, by: .advertisedService("FFF0"))
}
}
}
}
Once you have the Bluetooth
module configured within your Spezi app, you can access the module within your
Environment
.
You can use the scanNearbyDevices(enabled:with:minimumRSSI:advertisementStaleInterval:autoConnect:)
and autoConnect(enabled:with:minimumRSSI:advertisementStaleInterval:)
modifiers to scan for nearby devices and/or auto connect to the first available device. Otherwise, you can also manually start and stop scanning for nearby devices
using scanNearbyDevices(minimumRSSI:advertisementStaleInterval:autoConnect:)
and stopScanning()
.
To retrieve the list of nearby devices you may use nearbyDevices(for:)
.
Tip
To easily access the first connected device, you can just query the SwiftUI Environment for your BluetoothDevice
type.
Make sure to declare the property as optional using the respective Environment(_:)
initializer.
The below code example demonstrates all these steps of retrieving the Bluetooth
module from the environment, listing all nearby devices,
auto connecting to the first one and displaying some basic information of the currently connected device.
import SpeziBluetooth
import SwiftUI
struct MyView: View {
@Environment(Bluetooth.self)
var bluetooth
@Environment(MyDevice.self)
var myDevice: MyDevice?
var body: some View {
List {
if let myDevice {
Section {
Text("Device")
Spacer()
Text("\(myDevice.state.description)")
}
}
Section {
ForEach(bluetooth.nearbyDevices(for: MyDevice.self), id: \.id) { device in
Text("\(device.name ?? "unknown")")
}
} header: {
HStack {
Text("Devices")
.padding(.trailing, 10)
if bluetooth.isScanning {
ProgressView()
}
}
}
}
.scanNearbyDevices(with: bluetooth, autoConnect: true)
}
}
Tip
Use ConnectedDevices
to retrieve the full list of connected devices from the SwiftUI environment.
The previous section explained how to discover nearby devices and retrieve the currently connected one from the environment.
This is great ad-hoc connection establishment with devices currently nearby.
However, this might not be the most efficient approach, if you want to connect to a specific, previously paired device.
In these situations you can use the retrieveDevice(for:as:)
method to retrieve a known device.
Below is a short code example illustrating this method.
let id: UUID = ... // a Bluetooth peripheral identifier (e.g., previously retrieved when pairing the device)
let device = bluetooth.retrieveDevice(for: id, as: MyDevice.self)
await device.connect() // assume declaration of @DeviceAction(\.connect)
// Connect doesn't time out. Connection with the device will be established as soon as the device is in reach.
A Spezi Module
is a great way of structuring your application into
different subsystems and provides extensive capabilities to model relationship and dependence between modules.
Every BluetoothDevice
is a Module
.
Therefore, you can easily access your SpeziBluetooth device from within any Spezi Module
using the standard
Module Dependency infrastructure. At the same time,
every BluetoothDevice
can benefit from the same capabilities as every other Spezi Module
.
Below is a short code example demonstrating how a BluetoothDevice
uses the @Dependency
property to interact with a Spezi Module that is
configured within the Spezi application.
class Measurements: Module, EnvironmentAccessible, DefaultInitializable {
required init() {}
func recordNewMeasurement(_ measurement: WeightMeasurement) {
// ... process measurement
}
}
class MyDevice: BluetoothDevice {
@Service var weightScale = WeightScaleService()
// declare dependency to a configured Spezi Module
@Dependency var measurements: Measurements
required init() {}
func configure() {
weightScale.$weightMeasurement.onChange { [weak self] value in
self?.handleNewMeasurement(value)
}
}
private func handleNewMeasurement(_ measurement: WeightMeasurement) {
measurements.recordNewMeasurement(measurement)
}
}
For more information, please refer to the API documentation.
Contributions to this project are welcome. Please make sure to read the contribution guidelines and the contributor covenant code of conduct first.
This project is licensed under the MIT License. See Licenses for more information.