A lightweight Swift package for scanning and discovering Bluetooth Low Energy (BLE) peripherals on iOS using a modern callback-based architecture.
- 🔍 Generic BLE Scanning: Discover all BLE peripherals or filter by service UUIDs
- 📍 Location Tagging: Optionally tag discovered devices with GPS coordinates
- 📱 iOS 14+ Support: Built with modern Swift and CoreBluetooth APIs
- 🔋 Background Scanning: Optional support for background BLE scanning
- 📊 Advertisement Data: Capture manufacturer data, service UUIDs, and service data
- 🎯 RSSI Filtering: Filter devices by signal strength
- ⚡ Callback-Based API: Simple, modern callback architecture for easy integration
- 🧪 Unit Tested: Comprehensive test coverage
- iOS 14.0+
- Swift 5.9+
- Xcode 15.0+
Add the following to your Package.swift file:
dependencies: [
.package(url: "https://github.com/Jef-in/iOSBluetoothSDK", from: "1.0.0")
]Or in Xcode:
- File → Add Package Dependencies
- Enter the repository URL
- Select version 1.0.0 or later
import BLESDK
class MyViewController: UIViewController {
private var sdkManager: BLESDKManager!
override func viewDidLoad() {
super.viewDidLoad()
// Create SDK manager with default configuration
sdkManager = BLESDK.createManager()
// Set up callbacks
setupCallbacks()
}
private func setupCallbacks() {
sdkManager.onDeviceDiscovered = { [weak self] device in
print("New device: \(device.name ?? "Unknown")")
}
sdkManager.onDeviceUpdated = { [weak self] device in
print("Updated device: \(device.name ?? "Unknown")")
}
sdkManager.onScanningStateChanged = { [weak self] state in
print("Scanning state: \(state)")
}
sdkManager.onErrorEncountered = { [weak self] error in
print("Error: \(error.localizedDescription)")
}
}
}let config = SDKConfiguration(
serviceUUIDs: [CBUUID(string: "FFE0")], // Filter by specific services
enableLocationTracking: true, // Tag with GPS
allowBackgroundScanning: false, // Foreground only
rssiThreshold: -70 // Minimum signal strength
)
sdkManager = BLESDKManager(configuration: config)// Start scanning
sdkManager.startScanning()
// Stop scanning
sdkManager.stopScanning()
// Clear discovered devices
sdkManager.clearDevices()private func setupCallbacks() {
// Called when a new device is discovered
sdkManager.onDeviceDiscovered = { [weak self] device in
print("New device: \(device.name ?? "Unknown")")
print("RSSI: \(device.rssi)")
print("Manufacturer Data: \(device.manufacturerData?.hexString ?? "None")")
}
// Called when an existing device's data is updated
sdkManager.onDeviceUpdated = { [weak self] device in
print("Updated device: \(device.name ?? "Unknown")")
}
// Called when scanning state changes
sdkManager.onScanningStateChanged = { [weak self] state in
switch state {
case .scanning:
print("Scanning started")
case .stopped:
print("Scanning stopped")
case .paused:
print("Scanning paused")
}
}
// Called when an error occurs
sdkManager.onErrorEncountered = { [weak self] error in
print("Error: \(error.localizedDescription)")
}
}// Get all discovered devices
let devices = sdkManager.devices
// Get specific device
if let device = sdkManager.device(withId: deviceId) {
// Access device properties
print("Name: \(device.name ?? "Unknown")")
print("RSSI: \(device.rssi)")
print("Services: \(device.serviceUUIDs?.map { $0.uuidString } ?? [])")
// Get human-readable advertisement data
print(device.advertisedDataDescription)
}Each discovered device includes:
id: Unique peripheral identifier (UUID)name: Advertised device name (optional)rssi: Signal strength in dBmdiscoveredAt: Discovery timestamplocation: GPS coordinates if location tracking enabledmanufacturerData: Manufacturer-specific dataserviceUUIDs: Advertised service UUIDsserviceData: Service-specific datatxPowerLevel: Transmit power levelisConnectable: Whether device accepts connections
- serviceUUIDs: Filter scanning to specific service UUIDs (nil = scan all)
- enableLocationTracking: Tag devices with GPS coordinates (requires location permissions)
- allowBackgroundScanning: Enable background BLE scanning
- rssiThreshold: Minimum RSSI value for device filtering (nil = no filtering)
Add to your Info.plist:
<!-- Bluetooth permissions -->
<key>NSBluetoothAlwaysUsageDescription</key>
<string>We need Bluetooth to discover nearby devices</string>
<!-- Location permissions (if enableLocationTracking = true) -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>We need your location to tag discovered devices</string>The SDK defines these error cases:
bluetoothPoweredOff: Bluetooth is disabledbluetoothUnauthorized: Bluetooth permission deniedbluetoothUnsupported: Device doesn't support BluetoothlocationServicesDisabled: Location services disabledlocationUnauthorized: Location permission deniedscanningFailed: BLE scan failedconfigurationInvalid: Invalid configuration
Run tests using:
swift testOr in Xcode:
- Press ⌘U to run all tests
- View test coverage in the Coverage tab
The SDK provides four main callbacks:
-
onDeviceDiscovered: Called when a new BLE device is discovered- Type:
(BLEDevice) -> Void
- Type:
-
onDeviceUpdated: Called when an existing device's advertisement data changes- Type:
(BLEDevice) -> Void
- Type:
-
onScanningStateChanged: Called when the scanning state changes- Type:
(ScanningState) -> Void
- Type:
-
onErrorEncountered: Called when an error occurs- Type:
(BLESDKError) -> Void
- Type:
import SwiftUI
import BLESDK
class BLEScannerViewModel: ObservableObject {
@Published var devices: [BLEDevice] = []
@Published var isScanning: Bool = false
@Published var errorMessage: String?
private var manager: BLESDKManager?
init() {
setupManager()
}
private func setupManager() {
manager = BLESDK.createManager()
manager?.onDeviceDiscovered = { [weak self] device in
DispatchQueue.main.async {
self?.devices.append(device)
}
}
manager?.onDeviceUpdated = { [weak self] device in
DispatchQueue.main.async {
if let index = self?.devices.firstIndex(where: { $0.id == device.id }) {
self?.devices[index] = device
}
}
}
manager?.onScanningStateChanged = { [weak self] state in
DispatchQueue.main.async {
self?.isScanning = (state == .scanning)
}
}
manager?.onErrorEncountered = { [weak self] error in
DispatchQueue.main.async {
self?.errorMessage = error.localizedDescription
}
}
}
func startScan() {
devices.removeAll()
manager?.startScanning()
}
func stopScan() {
manager?.stopScanning()
}
}The SDK uses a callback-based architecture for simplicity and flexibility:
BLESDK/
├── BLESDK.swift # Main entry point with factory method
├── Core/
│ ├── BLESDKManager.swift # Main SDK manager with callbacks
│ ├── Callbacks.swift # Callback type definitions
│ └── SDKConfiguration.swift # Configuration options
├── Models/
│ ├── BLEDevice.swift # Device model with advertisement data
│ ├── LocationData.swift # GPS location wrapper
│ └── Enums.swift # ScanningState & BLESDKError
├── Scanner/
│ └── BLEScanner.swift # CoreBluetooth wrapper
├── Location/
│ └── LocationManager.swift # CoreLocation wrapper
└── Utilities/
├── Constants.swift # SDK constants
├── Extensions.swift # Helper extensions
└── Logger.swift # Logging utility
The SDK includes a comprehensive SwiftUI demo app that demonstrates:
- Device discovery and listing
- Real-time RSSI updates
- Advertisement data parsing
- Location tracking on maps
- Scanning state management
- Error handling
Run the demo app to see the SDK in action.
MIT License - See LICENSE file for details
- Fork the repository
- Create your feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
For issues, questions, or contributions, please open an issue on GitHub.