iOS & macOS Platform Guide
Comprehensive guide for Plugin.Bluetooth on iOS and macOS platforms using CoreBluetooth framework.
Table of Contents
- Overview
- Requirements
- CoreBluetooth Framework
- Configuration
- Feature Support
- Platform Limitations
- Best Practices
- Troubleshooting
Overview
iOS and macOS use Apple's CoreBluetooth framework, which provides a native, high-level abstraction over Bluetooth Low Energy operations. Plugin.Bluetooth wraps CoreBluetooth to provide a consistent cross-platform API while respecting Apple's platform conventions.
Key Characteristics
- System-Managed: Many operations (MTU, PHY, connection priority) are automatically optimized by iOS/macOS
- Stable & Reliable: CoreBluetooth is mature and well-tested
- Permissions Required: Explicit Info.plist entries required for all Bluetooth operations
- Background Support: Limited background execution capabilities
Supported Platforms
- iOS: 11.0+ (L2CAP support), 13.0+ (enhanced features)
- macOS: 10.13+ (High Sierra and later)
- Mac Catalyst: 13.0+
- tvOS: 11.0+
Requirements
Minimum Versions
- iOS: 11.0 or later (for L2CAP support)
- macOS: 10.13 (High Sierra) or later
- Xcode: 13.0 or later
- .NET: .NET 8.0+
Required Frameworks
CoreBluetooth.framework(automatically linked)Foundation.framework(automatically linked)
CoreBluetooth Framework
Architecture Overview
Plugin.Bluetooth maps to CoreBluetooth as follows:
| Plugin.Bluetooth | CoreBluetooth | Description |
|---|---|---|
IBluetoothScanner |
CBCentralManager |
Central role for scanning and connecting |
IBluetoothRemoteDevice |
CBPeripheral |
Represents a remote BLE device |
IBluetoothRemoteService |
CBService |
GATT service on remote device |
IBluetoothRemoteCharacteristic |
CBCharacteristic |
GATT characteristic |
IBluetoothRemoteDescriptor |
CBDescriptor |
GATT descriptor |
IBluetoothBroadcaster |
CBPeripheralManager |
Peripheral role for advertising |
IBluetoothRemoteL2CapChannel |
CBL2CAPChannel |
Direct L2CAP socket connection |
Implementation Details
The iOS/macOS implementation uses delegates and event-driven patterns:
CbCentralManagerWrapper- WrapsCBCentralManagerwith delegate callbacksCbPeripheralWrapper- WrapsCBPeripheralwith typed delegate methodsCbPeripheralManagerWrapper- WrapsCBPeripheralManagerfor broadcasting- Main thread dispatching for CoreBluetooth operations (iOS requirement)
Configuration
Info.plist Entries
Required - You must add these entries to your Info.plist file. Without them, your app will crash on first Bluetooth access.
Central Mode (Scanning & Connecting)
<!-- iOS 13+ Required -->
<key>NSBluetoothAlwaysUsageDescription</key>
<string>This app needs Bluetooth to communicate with BLE devices</string>
<!-- iOS 12 and earlier (deprecated but still needed for older iOS) -->
<key>NSBluetoothPeripheralUsageDescription</key>
<string>This app needs Bluetooth to communicate with BLE devices</string>
Peripheral Mode (Broadcasting)
<key>NSBluetoothAlwaysUsageDescription</key>
<string>This app needs Bluetooth to advertise as a peripheral device</string>
Background Modes (Optional)
If you need background BLE operations, add to Info.plist:
<key>UIBackgroundModes</key>
<array>
<string>bluetooth-central</string> <!-- For scanning/connecting in background -->
<string>bluetooth-peripheral</string> <!-- For broadcasting in background -->
</array>
Capabilities in Xcode
- Open your project in Xcode
- Select your target
- Go to "Signing & Capabilities"
- Add "Background Modes" capability if needed
- Check "Uses Bluetooth LE accessories" or "Acts as a Bluetooth LE accessory"
macOS Considerations
On macOS (including Mac Catalyst):
- Requires user approval on first Bluetooth access
- System shows permission dialog automatically
- No Info.plist entries needed for macOS apps (but use them for Mac Catalyst)
Feature Support
✅ Fully Supported Features
1. Scanning
var options = new ScanningOptions
{
// Apple-specific: scan for specific services
Apple = new AppleScanningOptions
{
// Only devices advertising these services will be discovered
ServiceUuids = new[] { serviceGuid }
}
};
await scanner.StartScanningAsync(options);
Capabilities:
- Service UUID filtering (recommended for battery efficiency)
- Advertisement data parsing (ServiceUUIDs, LocalName, ManufacturerData)
- RSSI reading during scan
2. Connection
var connectionOptions = new ConnectionOptions
{
Apple = new AppleConnectionOptions
{
NotifyOnConnection = true, // Alert when device connects (background)
NotifyOnDisconnection = true, // Alert when device disconnects
NotifyOnNotification = true, // Alert on incoming notifications
EnableTransportBridging = false, // iOS 13+: Dual-mode Classic+LE
RequiresAncs = false // iOS 13+: Require Apple Notification Center Service
}
};
await device.ConnectAsync(connectionOptions);
Connection Options Explained:
NotifyOnConnection: Shows alert when peripheral connects while app is suspendedNotifyOnDisconnection: Shows alert on unexpected disconnectionNotifyOnNotification: Shows alerts for characteristic notificationsEnableTransportBridging: Allow dual-mode Bluetooth (Classic + LE)RequiresAncs: Connection only succeeds if peripheral supports ANCS
3. GATT Operations
All standard GATT operations are fully supported:
- Service discovery:
await device.ExploreServicesAsync() - Characteristic discovery:
await service.ExploreCharacteristicsAsync() - Descriptor discovery:
await characteristic.ExploreDescriptorsAsync() - Read:
await characteristic.ReadValueAsync() - Write:
await characteristic.WriteValueAsync(data) - Write without response: Automatically uses if characteristic supports it
- Notifications:
await characteristic.StartListeningAsync() - Indications: Automatically handled same as notifications
4. MTU (Automatic)
// Get current MTU (read-only)
var currentMtu = device.Mtu;
// MTU cannot be requested - iOS negotiates automatically
// await device.RequestMtuAsync(512); // No-op on iOS
iOS MTU Behavior:
- Automatically negotiated during connection
- Typically 185 bytes for iPhone 8 and later
- Up to 512 bytes on newer devices (iPhone 12+)
- Cannot be changed programmatically
- Read via
device.Mtuproperty after connection
5. PHY (Physical Layer) - Read Only
// System automatically selects optimal PHY
// Read current PHY after connection
device.PhyChanged += (sender, args) =>
{
Console.WriteLine($"TX PHY: {args.TxPhy}"); // e.g., Le1M, Le2M
Console.WriteLine($"RX PHY: {args.RxPhy}");
};
iOS PHY Behavior:
- Automatically selected by system
- iOS 12+: Usually Le1M (1 Mbps)
- iOS/devices supporting Bluetooth 5.0: May use Le2M (2 Mbps)
- Cannot be set programmatically
- Available via
PhyChangedevent after connection
6. L2CAP Channels (iOS 11+)
// Open L2CAP channel for direct socket communication
await device.OpenL2CapChannelAsync(psm: 0x0080); // PSM must be advertised by peripheral
device.L2CapChannelOpened += async (sender, args) =>
{
var channel = args.Channel;
// Read current MTU for L2CAP channel
Console.WriteLine($"L2CAP MTU: {channel.Mtu}"); // Default: 672 bytes
// Write data directly to channel
await channel.WriteAsync(data);
// Read data from channel
var receivedData = await channel.ReadAsync();
// Subscribe to incoming data
channel.DataReceived += (s, e) => ProcessData(e.Data);
};
L2CAP Capabilities:
- Available on iOS 11.0+
- PSM (Protocol/Service Multiplexer) must be advertised by peripheral
- Higher throughput than GATT characteristics
- Lower latency
- Default MTU: 672 bytes (minimum guaranteed by Bluetooth spec)
- MTU cannot be changed on iOS (see L2CAP-Options.md)
7. RSSI (Signal Strength)
// Read RSSI during scanning (from advertisement)
scanner.DeviceListChanged += (sender, args) =>
{
foreach (var device in scanner.Devices)
{
Console.WriteLine($"RSSI: {device.SignalStrengthInDbm} dBm");
}
};
// Read RSSI after connection
await device.ReadSignalStrengthAsync();
device.SignalStrengthRead += (sender, args) =>
{
Console.WriteLine($"Connected RSSI: {args.SignalStrengthInDbm} dBm");
};
8. Broadcasting (Peripheral Mode)
var broadcaster = serviceProvider.GetRequiredService<IBluetoothBroadcaster>();
// Configure advertisement data
var options = new BroadcastingOptions
{
LocalName = "MyDevice",
ServiceUuids = new[] { serviceGuid }
};
// Start advertising
await broadcaster.StartAsync(options);
// Add GATT services
var service = await broadcaster.AddServiceAsync(serviceGuid);
var characteristic = await service.AddCharacteristicAsync(
characteristicGuid,
properties: CharacteristicProperties.Read | CharacteristicProperties.Notify,
permissions: CharacteristicPermissions.Readable
);
// Handle read requests from centrals
characteristic.ReadRequested += (sender, args) =>
{
args.RespondWithValue(data);
};
Broadcasting Features:
- Full GATT server implementation
- Support for multiple services and characteristics
- Read/Write/Notify/Indicate operations
- Handle incoming connections from central devices
- Monitor connected centrals via
ClientDeviceConnectedevent
⚠️ Automatic/Limited Features
Connection Priority
// No-op on iOS - system manages connection parameters automatically
await device.RequestConnectionPriorityAsync(BluetoothConnectionPriority.High);
iOS automatically adjusts connection parameters based on:
- Current power state (Low Power Mode affects this)
- App state (foreground vs. background)
- System load
- Peripheral requirements
Typical Connection Intervals:
- Active transfers: 15-30ms (high throughput)
- Idle connection: 100-200ms (power saving)
- Background: 1-2 seconds (significant battery savings)
❌ Not Supported
None. All major BLE features are supported on iOS/macOS, though some are automatic rather than controllable.
Platform Limitations
1. System-Managed Parameters
Impact: Cannot fine-tune performance
- MTU is negotiated automatically
- PHY is selected automatically
- Connection parameters adjust dynamically
- No control over connection interval, slave latency, or supervision timeout
Workaround: iOS manages these parameters automatically. No manual tuning is available.
2. Background Scanning Limitations
// Background scanning has restrictions:
// - Only finds devices advertising specific services
// - Slower scan rate (every 10-30 seconds vs. continuous)
// - Cannot use manufacturer data filtering
Background Scanning Rules:
- Must specify service UUIDs in scan options
- Scan operates on slower duty cycle
- Advertisement data may be coalesced/delayed
- CoreBluetooth handles background automatically
Best Practice: Use service UUID filtering even in foreground for better battery life.
3. Main Thread Requirements
CoreBluetooth requires many operations on main thread:
// Plugin.Bluetooth handles this automatically via MainThreadDispatcher
// You don't need to worry about threading
Internal implementation uses MainThreadDispatcher.BeginInvokeOnMainThread() for all CoreBluetooth calls.
4. Peripheral Caching
iOS caches peripheral information:
- Services/characteristics discovered once are cached
- Cache persists across connections
- May cause stale data if peripheral GATT database changes
Workaround: Not exposed in CoreBluetooth. Re-discovery doesn't help. Peripheral must change its GATT database version (advanced).
5. Write Without Response Queue Limits
// iOS has internal queue for write-without-response
// Writing too fast may fail silently
characteristic.WriteValueAsync(data); // May queue
// Wait for ready signal before next write
await device.WaitForReadyToSendWriteWithoutResponseAsync(timeout, cancellationToken);
iOS notifies via IsReadyToSendWriteWithoutResponse when ready for next write-without-response.
Best Practices
1. Info.plist Configuration
✅ Always add required entries BEFORE first run
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Clear description of why you need Bluetooth</string>
❌ Don't forget this - App will crash on first Bluetooth access without these entries.
2. Service UUID Filtering
✅ Use service filtering for better battery life
var options = new ScanningOptions
{
Apple = new AppleScanningOptions
{
ServiceUuids = new[] { myServiceGuid } // Reduces power consumption
}
};
3. Background Operation
✅ Minimal operations in background
- Keep connections alive with periodic reads/writes
- Use notifications/indications instead of polling
- Batch operations where possible
❌ Don't scan continuously in background - OS will throttle and drain battery
4. Connection Reliability
✅ Monitor connection state changes
device.ConnectionStateChanged += (sender, args) =>
{
if (args.ConnectionState == ConnectionState.Disconnected)
{
// Handle unexpected disconnection
// Attempt reconnection if needed
}
};
5. Error Handling
try
{
await device.ConnectAsync(options);
}
catch (DeviceFailedToConnectException ex)
{
// Log and retry
// iOS error details in ex.NativeError
}
6. Test on Physical Devices
⚠️ iOS Simulator limitations:
- No actual Bluetooth hardware
- Cannot test real BLE operations
- Dummy implementations only
Always test on real iPhone/iPad/Mac with Bluetooth enabled.
7. Low Energy Mode Considerations
iOS Low Energy Mode affects BLE:
- Slower scan rates
- Longer connection intervals
- Background operations may be suspended
Test your app with Low Energy Mode enabled.
8. L2CAP Channel Management
// Always close L2CAP channels when done
await channel.CloseAsync();
await channel.DisposeAsync();
// Monitor channel closure
channel.Closed += (sender, args) =>
{
// Peripheral closed channel or connection lost
};
Troubleshooting
App Crashes on First Bluetooth Access
Problem: Missing Info.plist entries
Solution:
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Your description here</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>Your description here</string>
Can't Find Device During Scan
Possible Causes:
- Device not advertising
- Device advertising services not in scan filter
- Bluetooth disabled on iOS device
- Background scanning without service UUID filter
Solution:
- Verify device is advertising
- Check service UUIDs match
- Enable Bluetooth in Settings
- Add service UUID filter for background scanning
Connection Immediately Disconnects
Possible Causes:
- Device out of range
- Device rejected connection (pairing required, etc.)
- iOS Low Energy Mode active
- Too many concurrent connections
Solution:
- Check signal strength (RSSI > -85 dBm recommended)
- Verify peripheral accepts connections
- Test without Low Energy Mode
- Limit to 7-10 concurrent connections
MTU Lower Than Expected
Problem: iOS negotiated smaller MTU than desired
Reality:
- MTU depends on iOS version and device hardware
- iPhone 8+: Typically 185 bytes
- iPhone 12+: Up to 512 bytes
- Cannot be forced higher
Solution: Design for minimum 23-byte MTU, scale up as available
Write Without Response Fails
Problem: Queue full on iOS
Solution: Use WaitForReadyToSendWriteWithoutResponseAsync()
await device.WaitForReadyToSendWriteWithoutResponseAsync(
timeout: TimeSpan.FromSeconds(5),
cancellationToken
);
await characteristic.WriteValueAsync(nextPacket);
L2CAP Connection Fails
Possible Causes:
- iOS version < 11.0
- PSM not advertised by peripheral
- PSM reserved for system use
Solution:
- Check iOS version
- Verify peripheral advertises PSM in GATT or advertisement
- Use PSM in dynamic range (0x0080 - 0x00FF recommended)
Background Scanning Not Working
Problem: Not finding devices in background
Requirements:
- Add
bluetooth-centralto UIBackgroundModes - Specify service UUIDs in scan options
- User granted "Always" permission
Solution:
var options = new ScanningOptions
{
Apple = new AppleScanningOptions
{
ServiceUuids = new[] { serviceGuid } // Required for background
}
};
Permission Denied Error
Problem: User denied Bluetooth permission
Solution:
scanner.StateChanged += (sender, args) =>
{
if (args.NewState == AdapterState.Unauthorized)
{
// Show UI directing user to Settings > Your App > Bluetooth
// Cannot request permission again programmatically
}
};
User must manually enable in Settings app.
Additional Resources
Apple Documentation
- CoreBluetooth Programming Guide
- CBCentralManager Reference
- CBPeripheral Reference
- CBPeripheralManager Reference
- CBL2CAPChannel Reference