iOS BLE Deep Dive

Status: Pre-prototype reference document. iOS BLE background behavior is the #1 technical risk to this project. Read this before writing any iOS code.


1. The Core iOS BLE Problem

Why iOS Is Fundamentally Different from Android

On Android, an app can declare a foreground service and maintain a long-running background process with essentially unrestricted BLE scanning and advertising for as long as the service runs. The operating system does not systematically terminate these processes unless memory pressure forces it, and even then, services are typically restarted.

iOS operates on an entirely different philosophy. Apple's privacy model is built around the principle that apps should not run arbitrary background services. iOS aggressively manages process lifecycle: apps are suspended within seconds of moving to the background and are terminated when the system needs resources. There is no equivalent to Android's foreground service that allows a Bluetooth app to simply "keep running."

This is not a bug — it is intentional architecture. Apple's goal is to prevent battery drain, prevent user surveillance, and maintain a predictable security perimeter. For a privacy-first proximity app, the irony is that the platform with the strongest privacy branding is also the platform that makes privacy-preserving proximity detection hardest to build.

CBCentralManager in Background: What Actually Happens

CBCentralManager is the CoreBluetooth class for scanning — the "central" role that discovers advertising peripherals. When the app moves to the background without declaring any background modes, CBCentralManager delivers no scan results at all. The scanner is effectively frozen.

With the bluetooth-central UIBackgroundMode declared, CoreBluetooth does continue scanning in the background, but with a substantially reduced duty cycle. The exact duty cycle is not documented by Apple and varies by hardware and iOS version, but the practical effect is:

With no bluetooth-central background mode declared, the app receives zero BLE events when backgrounded. This is absolute.

CBPeripheralManager in Background: What Actually Happens

CBPeripheralManager is the CoreBluetooth class for advertising — the "peripheral" role that broadcasts to nearby centrals. When backgrounded:

The practical consequence is that a backgrounded iOS peripheral cannot be discovered by an Android central scanning for its service UUID in the normal advertisement payload. The UUID is only present in the overflow area, which only iOS devices know how to read.

The Overflow Area: iOS-to-iOS Only

Apple's overflow area is a proprietary mechanism introduced to work around their own advertisement stripping. When a backgrounded iOS peripheral cannot fit its service UUIDs into the normal advertisement packet (because the payload has been stripped), those UUIDs are encoded into a special manufacturer-specific data field that Apple calls the overflow area.

The overflow area encoding:

This means: a backgrounded iOS peripheral advertising via CoreBluetooth with bluetooth-peripheral declared is effectively invisible to Android. Only another iOS device running a CoreBluetooth central that knows to look in the overflow area can find it.

There is no workaround for this at the CoreBluetooth API level. It is a platform constraint.

Duty Cycle Reduction: The Numbers

Apple has never published the exact duty cycle used for background BLE scanning. Empirical data from developers and researchers suggests the following:

For a passive contact-detection scenario (detect when a known contact comes within range), this means detection latency in the background can range from several seconds to over a minute, depending on conditions. This is still potentially acceptable for "was this person near me today" use cases, but not for real-time proximity alerts.


2. Per-Version Behavior Reference Table

The following documents observed and reported behavior. Apple does not publish BLE background behavior changelogs. Sources are developer reports, open-source project issue trackers, and Apple developer forum threads.

iOS Version Background Scan Results Background Advertising Time Before Callbacks Stop Known Issues / Notes
iOS 15.x Delivered with duty cycle reduction (~2–5s intervals observed). Reasonably reliable with bluetooth-central. Overflow area active. Service UUIDs stripped from primary packet. Local name stripped. No hard time limit documented; delivery continues with increasing latency Baseline behavior. Most BLE-background tutorials written against this era. Foreground-to-background transition has ~1–2s gap.
iOS 16.x Behavior largely unchanged from iOS 15. Some reports of slightly improved reliability for scan delivery while screen-locked. No documented change. Overflow area behavior unchanged. No hard time limit reported iOS 16.2+ introduced some Core Bluetooth stability fixes per release notes. No major regressions reported by BLE developers.
iOS 17.0–17.2 Early iOS 17 reports of intermittent scan delivery failures in background. Some users reported CBCentralManager entering unknown state after extended background operation. No documented change to overflow area. iOS 17.0–17.1 had reports of callbacks stopping after ~10 minutes in certain conditions iOS 17.0 introduced some background task scheduling changes. BLE-heavy apps (Berty, p2panda evaluations) reported increased instability. Partially addressed in 17.2.
iOS 17.3–17.6 Stabilized. Background scan delivery considered comparable to iOS 15/16 baseline. No change. No hard limit; state restoration mechanism recommended as mitigation. 17.4 introduced some privacy-related changes to background capability usage descriptions enforcement. App Store review more strict about declared background modes.
iOS 18.0–18.0.1 Reports of regression: background scan delivery gaps of 30–60 seconds observed even with bluetooth-central declared. Connected devices (already paired GATT connections) appear unaffected. Initial reports of advertising instability — some apps seeing CBPeripheralManager not restart after system termination despite state restoration. Some reports of ~5 minute hard cutoff on scan event delivery for inactive (no active connections) scanning iOS 18 introduced significant changes to background task execution. Multiple open-source BLE projects logged new issues.
iOS 18.1 Partial improvement. Scan delivery gaps reduced but still higher latency than iOS 17.x baseline. Advertising stabilized. Less frequent hard cutoffs reported; ~10+ minutes before delivery stops without connections Berty team and other BLE-focused developers noted iOS 18.1 as improvement over 18.0 but still not back to iOS 17 behavior.
iOS 18.2 Further stabilization. Background scanning generally functional with bluetooth-central. Some developers report near-parity with iOS 17.6. No reported change. No consistent hard limit reported in 18.2 Multipeer Connectivity has a reported regression in iOS 18 specifically: reliable connections limited to approximately 2m. This is separate from CoreBluetooth. See Section 7.
iOS 19 (2026, projected) No public beta at time of writing (April 2026). WWDC 2026 not yet held. No documented behavior changes. Unknown. Unknown. Apple has shown interest in improving background execution for health and fitness apps (AirPods Pro, Apple Watch context). Some speculation that WWDC 2026 may introduce more permissive background BLE for specific entitlement categories. Unconfirmed.

The iPhone 17 Device Regression (2026)

Separate from iOS version behavior, a hardware-specific regression has been reported on iPhone 17 devices (released September 2025) running iOS 18.x:

Reported behavior: Background BLE scanning stops delivering results immediately upon the app moving to the background, even when bluetooth-central is declared in UIBackgroundModes. The CBCentralManager does not enter an error state — it continues to report CBManagerStatePoweredOn — but centralManager(_:didDiscover:advertisementData:rssi:) is not called while the app is backgrounded.

Scope: Reports are specific to iPhone 17 (A18 Pro chip, new Bluetooth 5.4 hardware). iPhone 16 and iPhone 15 devices running the same iOS version do not show this behavior.

Current status (April 2026): Confirmed in multiple developer reports on Apple Developer Forums and in at least one open GitHub issue on a BLE-focused library. No Apple acknowledgment. No fix released. Workaround being explored: iBeacon monitoring appears unaffected (because it is handled at the OS level, not app level — see Section 5).

Impact on this project: If confirmed and not fixed, this makes CoreBluetooth background scanning unreliable on iPhone 17 hardware. The iBeacon monitoring fallback (Section 5) becomes more important, not optional.


3. CoreBluetooth Background Modes Explained

bluetooth-central in UIBackgroundModes

What it is: A key added to the UIBackgroundModes array in Info.plist that tells iOS your app uses CoreBluetooth in the central role and needs continued operation when backgrounded.

What it enables:

What it does NOT enable:

What happens without it:

bluetooth-peripheral in UIBackgroundModes

What it is: The equivalent key for the peripheral role — apps that advertise to other devices.

What it enables:

What it does NOT enable:

What happens without it:

Declaring Both Background Modes in Info.plist

An app needing both roles (which is typical — you want to both discover others and be discoverable) must declare both:

<key>UIBackgroundModes</key>
<array>
  <string>bluetooth-central</string>
  <string>bluetooth-peripheral</string>
</array>

This goes in Info.plist. In Xcode, this is also configurable via the "Signing & Capabilities" tab → "Background Modes" capability → check both "Uses Bluetooth LE accessories" (central) and "Acts as a Bluetooth LE accessory" (peripheral).

Note: "Uses Bluetooth LE accessories" in Xcode's UI maps to bluetooth-central. The naming is confusing because it implies accessory communication, but this is the key for any background central-role scanning.

NSBluetoothAlwaysUsageDescription

Required since iOS 13 for any app using CoreBluetooth (both central and peripheral). If this key is absent from Info.plist, the app crashes when attempting to initialize CBCentralManager or CBPeripheralManager on iOS 13+.

<key>NSBluetoothAlwaysUsageDescription</key>
<string>This app uses Bluetooth to discover nearby contacts you have previously met in person. No location information is transmitted.</string>

App Review requirements:

What Apple looks for in App Review when background modes are declared:

  1. The usage description must explicitly mention why background Bluetooth is needed — not just that the app uses Bluetooth
  2. Review notes (the "Notes for Review" field in App Store Connect) should explain the use case: "The app discovers nearby contacts who have previously met the user in person. Background mode allows this discovery to work when the user is not actively using the app, for example when both users have their phones in their pockets."
  3. The app should actually use the declared capability. An app that declares background modes but only uses Bluetooth in the foreground will likely be flagged
  4. Apple has increased scrutiny of background mode declarations since iOS 17. Apps that have passed review in the past have been flagged upon update submission

4. State Preservation and Restoration

State preservation and restoration is CoreBluetooth's primary mechanism for surviving app termination while still reacting to BLE events. Without it, a terminated app that had active scans or connections will lose all that state and never receive BLE callbacks until manually relaunched by the user.

How It Works

When state preservation keys are provided at CBCentralManager or CBPeripheralManager initialization, iOS takes on the responsibility of tracking that manager's state. If the app is terminated (by the system due to memory pressure, or by the user via the app switcher), iOS preserves:

When a relevant BLE event occurs (a scan result matching a preserved UUID, a connection state change, a characteristic notification), iOS relaunches the app in the background with a short execution window (typically 10 seconds, extendable once via beginBackgroundTask(expirationHandler:)). The app is launched directly into application(_:didFinishLaunchingWithOptions:) with launch options indicating it was relaunched for Bluetooth.

Keys to Set

For Central:

let centralManager = CBCentralManager(
    delegate: self,
    queue: nil,
    options: [CBCentralManagerOptionRestoreIdentifierKey: "com.yourapp.central"]
)

For Peripheral:

let peripheralManager = CBPeripheralManager(
    delegate: self,
    queue: nil,
    options: [CBPeripheralManagerOptionRestoreIdentifierKey: "com.yourapp.peripheral"]
)

The restore identifier string is arbitrary but must be unique within your app. It is used by the system to match the preserved state to the new manager instance upon relaunch.

The willRestoreState Delegate Method

Upon relaunch, before centralManagerDidUpdateState is called, the delegate receives:

func centralManager(
    _ central: CBCentralManager,
    willRestoreState dict: [String: Any]
) {
    // dict[CBCentralManagerRestoredStatePeripheralsKey]: [CBPeripheral]
    // Previously connected or connecting peripherals
    let restoredPeripherals = dict[CBCentralManagerRestoredStatePeripheralsKey]
        as? [CBPeripheral] ?? []

    // dict[CBCentralManagerRestoredStateScanServicesKey]: [CBUUID]
    // Service UUIDs that were being scanned for
    let restoredScanServices = dict[CBCentralManagerRestoredStateScanServicesKey]
        as? [CBUUID] ?? []

    // dict[CBCentralManagerRestoredStateScanOptionsKey]: [String: Any]
    // Options that were passed to scanForPeripherals
    let restoredScanOptions = dict[CBCentralManagerRestoredStateScanOptionsKey]
        as? [String: Any] ?? [:]

    // Reconnect to previously connected peripherals
    for peripheral in restoredPeripherals {
        peripheral.delegate = self
        central.connect(peripheral, options: nil)
    }

    // Store restored scan info — re-scan after state is powered on
    self.pendingScanServices = restoredScanServices
    self.pendingScanOptions = restoredScanOptions
}

func centralManagerDidUpdateState(_ central: CBCentralManager) {
    guard central.state == .poweredOn else { return }

    // Resume scanning if we had a pending restore
    if let services = pendingScanServices {
        central.scanForPeripherals(
            withServices: services,
            options: pendingScanOptions
        )
        pendingScanServices = nil
    }
}

For the peripheral role:

func peripheralManager(
    _ peripheral: CBPeripheralManager,
    willRestoreState dict: [String: Any]
) {
    // dict[CBPeripheralManagerRestoredStateServicesKey]: [CBMutableService]
    let restoredServices = dict[CBPeripheralManagerRestoredStateServicesKey]
        as? [CBMutableService] ?? []

    // dict[CBPeripheralManagerRestoredStateAdvertisementDataKey]: [String: Any]
    let restoredAdvertisement = dict[CBPeripheralManagerRestoredStateAdvertisementDataKey]
        as? [String: Any] ?? [:]

    // Restore service references and re-add if not already present
    self.pendingServices = restoredServices
    self.pendingAdvertisement = restoredAdvertisement
}

func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
    guard peripheral.state == .poweredOn else { return }

    for service in pendingServices {
        peripheral.add(service)
    }

    if !pendingAdvertisement.isEmpty {
        peripheral.startAdvertising(pendingAdvertisement)
    }
}

Detecting Background Relaunch in AppDelegate

func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {

    let bluetoothLaunch = launchOptions?[.bluetooth] != nil
    let peripheralLaunch = launchOptions?[.bluetoothPeripheral] != nil

    if bluetoothLaunch || peripheralLaunch {
        // App was relaunched by the system for a BLE event
        // Do minimal work — execution window is short (~10 seconds)
        // Initialize CBCentralManager / CBPeripheralManager with restore keys
        // willRestoreState will be called immediately after init
        initializeBluetoothManagers()
    } else {
        // Normal launch — initialize as usual
        initializeBluetoothManagers()
    }

    return true
}

Critical Pitfalls

Execution window is short: When relaunched in the background, you have approximately 10 seconds of execution time. Use beginBackgroundTask(expirationHandler:) to request an extension (up to ~30 seconds on most devices), but this is not guaranteed. Do not attempt to show UI, perform network requests, or do heavy computation in this window. Only reinitialize CoreBluetooth managers, process the incoming BLE event, and persist any state to disk.

No guarantee of delivery: State restoration does not guarantee that the app will be relaunched for every BLE event. The system makes a best-effort attempt. If the system is under memory pressure or the phone is in a deep power-saving state, relaunch may not occur.

User can break state restoration: If the user manually terminates the app from the app switcher (swipe up to quit), state restoration is disabled. iOS treats a user-initiated termination as an explicit signal that the app should not relaunch. There is no API workaround for this.

Restoration vs. relaunch is not the same: State restoration means the CoreBluetooth framework restores manager state. It does not mean the app gets to run arbitrarily. The relaunch is into a background execution context, not a foreground context.

Events that trigger relaunch:

Events that do NOT trigger relaunch:


5. CoreLocation iBeacon Monitoring — The Best Workaround

How iBeacon Monitoring Works

iBeacon is an Apple-designed BLE advertisement profile that encodes a UUID (16 bytes), major value (2 bytes), and minor value (2 bytes) into the advertisement packet. The standard was introduced with iOS 7.

The key architectural insight: iBeacon monitoring is implemented at the OS level, not the app level. The iOS location subsystem (CoreLocation, not CoreBluetooth) is responsible for detecting iBeacons. This means the app does not need to be running at all for iOS to detect a beacon. The OS is always watching.

When a beacon matching a registered CLBeaconRegion is detected, CoreLocation will:

  1. Deliver a didEnterRegion callback if the app is in the foreground or background
  2. Relaunch a terminated app into the background with a short execution window
  3. Deliver a didExitRegion callback when the beacon is no longer detected

The monitoring API:

let beaconUUID = UUID(uuidString: "YOUR-APP-WIDE-UUID-HERE")!

// Monitor for any beacon with this UUID (any major/minor)
let beaconRegion = CLBeaconRegion(
    uuid: beaconUUID,
    identifier: "com.yourapp.contactbeacon"
)
beaconRegion.notifyOnEntry = true
beaconRegion.notifyOnExit = true
beaconRegion.notifyEntryStateOnDisplay = true // Deliver on screen unlock

let locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.startMonitoring(for: beaconRegion)

Detection callbacks:

func locationManager(
    _ manager: CLLocationManager,
    didEnterRegion region: CLRegion
) {
    guard let beaconRegion = region as? CLBeaconRegion else { return }
    // A beacon with our app UUID is within range
    // App may have been relaunched in background to deliver this
    // Begin CoreBluetooth contact identification (foreground or short background window)
    handleProximityDetected()
}

func locationManager(
    _ manager: CLLocationManager,
    didExitRegion region: CLRegion
) {
    // Beacon is no longer detected
    handleProximityLost()
}

Why This Works Differently from CoreBluetooth Background Scanning

When iBeacon monitoring is active, iOS itself is watching for the registered UUID using the Bluetooth hardware, regardless of whether your app is running. This is structurally different from CoreBluetooth background scanning, where your app's CBCentralManager is doing the scanning with a duty-cycle-reduced scan.

The OS-level monitoring is:

Latency and Reliability

Detection latency: iBeacon region entry detection typically takes 5–30 seconds from when the beacon first appears. This is a hardware + firmware decision by Apple — the OS does not scan continuously for beacons (that would be too power-intensive), so there is inherent detection delay.

Exit detection latency: Exit detection is slower, typically 20–60 seconds after the beacon stops advertising. This is because the OS needs to verify the beacon is truly gone (not just momentarily occluded) before declaring exit.

Reliability in practice: iBeacon monitoring is the most reliable background detection mechanism on iOS. Projects like AirTag proximity detection (though not public API), retail proximity analytics, and museum guide apps have relied on this for years. It works.

App Relaunch for Terminated Apps

When a terminated app's beacon region is entered, iOS relaunches the app with:

Detection in AppDelegate:

func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {

    if launchOptions?[.location] != nil {
        // Could be iBeacon OR geofence trigger
        // Initialize CLLocationManager — didEnterRegion will fire immediately
        locationManager = CLLocationManager()
        locationManager.delegate = self
        // The delegate will receive didEnterRegion momentarily
    }

    return true
}

Entitlements and Permissions Required

iBeacon monitoring requires CLLocationManager with Always authorization. This is the most invasive location permission on iOS — it allows the app to access location data at any time, even when not in use.

The permission flow:

  1. App must include NSLocationWhenInUseUsageDescription (required for the initial prompt)
  2. App must include NSLocationAlwaysAndWhenInUseUsageDescription (required for Always permission)
  3. User grants "When In Use" first (iOS does not allow jumping directly to "Always")
  4. App calls requestAlwaysAuthorization() — iOS prompts to upgrade to "Always"
  5. Without "Always" authorization, startMonitoring(for:) will not deliver events to a backgrounded/terminated app

The Info.plist entries:

<key>NSLocationWhenInUseUsageDescription</key>
<string>Location permission is used to detect nearby contacts via Bluetooth proximity. No GPS data is collected or stored.</string>

<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Background location permission allows the app to detect when a known contact is nearby, even when you are not actively using the app. Only Bluetooth signal presence is used — no GPS coordinates are recorded.</string>

The Privacy Tradeoff: Location Permission for BLE Detection

This is a genuine tension in the design. The app's stated goal is structural privacy and no PII collection. Requiring location permission — especially "Always" — triggers immediate user concern and App Store scrutiny.

The technical reality: iBeacon monitoring uses Bluetooth hardware, not GPS. The CLLocationManager APIs are used because Apple chose to route iBeacon through CoreLocation (for historical reasons related to geofencing parity). When you use startMonitoring(for:) with a CLBeaconRegion, the device:

The permission label says "Location" because that is how Apple classifies the API. The actual capability being used is Bluetooth detection.

Can we use this without collecting location data? Yes. The app can use startMonitoring(for:) and startRangingBeacons(satisfying:) without ever accessing manager.location, without requesting GPS fixes, and without any network calls. The CLLocationManager delegate only receives beacon-related callbacks (didEnterRegion, didExitRegion, didRange). No latitude/longitude is involved.

How to explain this to users: The permission dialog text must be honest. The recommended framing is:

"This app uses Bluetooth to detect when people you know are nearby. iOS requires location permission to use this Bluetooth feature. No GPS location is collected or stored — only the Bluetooth presence of your contacts."

This framing is honest, accurate, and has been used by other proximity apps that have passed App Review.

App Store Review implications: Declaring NSLocationAlwaysAndWhenInUseUsageDescription and using it for iBeacon monitoring is an accepted App Store pattern. Apps like visitor guides, asset tracking tools, and retail apps use this routinely. The review team will scrutinize whether the "Always" permission is actually needed — in review notes, explain: "We use iBeacon monitoring to detect nearby contacts when the app is backgrounded. This requires Always authorization per CoreLocation documentation."

UUID Strategy

The iBeacon UUID is not a user identifier — it is a filter key. All devices running your app advertise the same UUID. The UUID just tells iOS "this advertisement is from our app."

Stable per-app UUID: E20A39F4-73F5-4BC4-A12F-17D1AD07A961
   (Generate once, never change, ship in the app)

Major value: Could encode app version or network shard (0–65535)
Minor value: Could encode a rotating short-term identifier (0–65535)

Privacy design with major/minor values:

Monitoring vs. Ranging

For passive background contact detection, monitoring is correct. Ranging is only appropriate when the user has opened the app and is actively trying to determine "how close is this contact."

iOS 17+ Behavior

No documented breaking changes to iBeacon monitoring in iOS 17 or 18. The mechanism is considered stable by Apple. The iPhone 17 hardware regression (Section 2) does not appear to affect iBeacon monitoring because the monitoring is handled by the CoreLocation daemon, not by the app's CBCentralManager.


6. Berty's Approach — Reference Implementation

Background and Architecture

Berty is an open-source, privacy-first messaging app built on libp2p. Their transport layer (Wesh Network) is designed to work over multiple physical transports including BLE, Multipeer Connectivity, and internet relays. Their approach to iOS BLE background behavior is the most thoroughly documented in the open-source space and represents the current state of the art.

Three-Transport Strategy

Berty's approach for iOS separates the problem by device pair type:

iOS-to-iOS: Multipeer Connectivity (MPC) is preferred. MPC uses AWDL (Apple Wireless Direct Link) under the hood on iOS. For iOS-to-iOS within proximity, MPC is more reliable than CoreBluetooth because it gets better OS treatment and Apple engineers have explicitly said it is the right API for iOS peer-to-peer. However, MPC has its own background restrictions.

iOS-to-Android (cross-platform): Custom CoreBluetooth GATT service. Berty defines a specific GATT service UUID and characteristic layout that both platforms can speak. The Android side uses standard Android BLE APIs. The iOS side uses CoreBluetooth with both background modes declared.

iOS-to-iOS over internet relay: When BLE and MPC fail, messages are routed through an optional internet relay (Tor or a rendezvous server). This is a fallback, not the primary path.

gomobile Binding Strategy

Berty's core protocol logic is written in Go and compiled for iOS using gomobile bind. The Go layer implements the libp2p protocol stack. The iOS-specific transport adapters are written in Swift/Objective-C and bridge into the Go layer.

This architecture matters for BLE background behavior: the Go runtime continues running while the iOS app has any background execution time, but it is still subject to iOS's background execution limits. The gomobile approach does not grant additional background privileges — it is constrained by the same bluetooth-central/bluetooth-peripheral background modes as any other iOS app.

BLE GATT Service Structure for Cross-Platform

Berty's BLE GATT profile (from their public source and blog posts):

This is a standard bidirectional communication pattern over GATT: each device acts as both central and peripheral simultaneously (the "dual role" pattern), allowing full-duplex communication.

Known Issues Documented by Berty

From their public issue tracker and blog posts:

  1. iOS background advertising is invisible to Android: Confirmed the overflow area issue. Their workaround: they use a rotating service UUID that is placed in the primary advertisement when in the foreground. When backgrounded, they accept that Android cannot see them and rely on Android devices to be the aggressors (Android scans and finds iOS only when iOS is in the foreground, or through iBeacon as a wake trigger).

  2. MPC in iOS 18 regression: Berty documented reduced MPC reliability in iOS 18, with connections requiring devices to be within approximately 1–2 meters. They have noted potential migration to Network.framework.

  3. Background scan delivery gaps: In their telemetry, they observe gaps of 1–3 minutes in background scan delivery under some conditions. Their mitigation is aggressive use of GATT connection persistence — once connected to a peer, they keep the connection alive as long as possible, because connection notifications (rather than new scan discoveries) are more reliably delivered in the background.

  4. Terminated app relaunch unreliability: Berty uses state restoration but acknowledges that relaunch does not always occur. Their protocol is designed to be tolerant of missed events — message sync is opportunistic, not guaranteed-delivery.

What Berty's Approach Tells Us

The takeaway from Berty's years of work on this problem:


7. Apple Multipeer Connectivity — When to Use It

What MPC Provides

Multipeer Connectivity (MultipeerConnectivity.framework) abstracts over three transports:

  1. Infrastructure WiFi (when both devices are on the same network)
  2. Peer-to-peer WiFi (AWDL — Apple Wireless Direct Link)
  3. Bluetooth LE (as fallback for discovery when WiFi is not available)

The framework handles transport selection automatically, picking the highest-bandwidth available option. For two iOS devices near each other, it typically uses AWDL, which provides WiFi-like speeds over a direct device-to-device link.

API surface:

// Advertising
let advertiser = MCNearbyServiceAdvertiser(
    peer: localPeerID,
    discoveryInfo: ["version": "1"],
    serviceType: "yourapp-svc"  // max 15 chars, lowercase alphanumeric + hyphens
)
advertiser.delegate = self
advertiser.startAdvertisingPeer()

// Browsing
let browser = MCNearbyServiceBrowser(
    peer: localPeerID,
    serviceType: "yourapp-svc"
)
browser.delegate = self
browser.startBrowsingForPeers()

What you get:

Why It Cannot Be Used for Cross-Platform

MPC is Apple-only and opaque. There is no public protocol specification. Android, Linux, or any non-Apple device cannot participate in an MPC session. The underlying AWDL protocol is proprietary, and while it has been reverse-engineered by researchers (the OWL project), those implementations are not production-ready and not suitable for a shipping app.

Additionally, MPC does not give you control over the advertisement payload. The discoveryInfo dictionary is visible to nearby browsers, but it must be treated as potentially readable by any iOS device running an app with the same service type string. You cannot embed cryptographic identifiers in the discovery info without custom encoding.

The iOS 18+ Stability Regression

Multiple developers and the Berty team have reported that Multipeer Connectivity in iOS 18 is significantly less stable than in iOS 17:

Apple has acknowledged general MPC improvements as part of their recommendation to migrate to Network.framework for new development. Their documented stance is that MPC should be used for "prototyping and simple use cases" and that production apps should use Network.framework.

There is no public fix timeline for the iOS 18 regression. The regression appears to be in the AWDL layer, not in MPC's API surface.

Apple's Recommendation: Network.framework

Apple's current (2025–2026) official position is:

See Section 8 for Network.framework details.

Tradeoff Table

CoreBluetooth Multipeer Connectivity Network.framework P2P
Cross-platform Yes (with custom GATT) No (Apple only) Partial (mDNS is open)
Background discovery Limited (duty cycle reduced) Very limited Better than MPC
Background data transfer Yes (with GATT notify) Unreliable in iOS 18 Limited but more stable
Transfer speed ~1 Mbps theoretical Up to WiFi speeds (AWDL) Up to WiFi speeds
Protocol control Full None Full
Privacy You control advertisement discoveryInfo is cleartext mDNS name is visible (opaque naming possible)
API complexity High Low Medium
Stability (iOS 18) Stable (with caveats) Regressed Stable
Use case fit Passive contact discovery iOS-to-iOS simple sync iOS-to-iOS bulk transfer

Recommendation: Use CoreBluetooth for discovery (cross-platform), Network.framework P2P for bulk transfer (iOS-to-iOS), and avoid MPC until the iOS 18 regression is resolved.


8. Network.framework with Peer-to-Peer WiFi

Overview

Network.framework is Apple's modern networking stack, introduced in iOS 12. It exposes NWBrowser for service discovery and NWListener for service advertising, with native support for peer-to-peer WiFi via Bonjour/mDNS.

Unlike CoreBluetooth, Network.framework peer-to-peer uses WiFi (AWDL for Apple-to-Apple, infrastructure WiFi or mDNS for broader discovery). This gives significantly higher bandwidth than BLE once a connection is established.

NWBrowser for Discovery

let parameters = NWParameters()
parameters.includePeerToPeer = true  // enables AWDL + Bonjour

let browser = NWBrowser(
    for: .bonjourWithTXTRecord(type: "_yourapp._tcp", domain: nil),
    using: parameters
)
browser.browseResultsChangedHandler = { results, changes in
    for change in changes {
        switch change {
        case .added(let result):
            // New peer discovered
            handleNewPeer(result)
        case .removed(let result):
            // Peer left
            handlePeerLeft(result)
        default:
            break
        }
    }
}
browser.stateUpdateHandler = { state in
    // Handle .ready, .failed, .cancelled
}
browser.start(queue: .main)

NWListener for Advertising

let parameters = NWParameters.tcp
parameters.includePeerToPeer = true

let listener = try? NWListener(using: parameters)
listener?.service = NWListener.Service(
    name: deviceOpaqueName,  // see privacy note below
    type: "_yourapp._tcp"
)
listener?.newConnectionHandler = { connection in
    // Incoming peer connection
    handleIncomingConnection(connection)
}
listener?.stateUpdateHandler = { state in
    // Handle .ready, .failed
}
listener?.start(queue: .main)

Background Behavior

Network.framework peer-to-peer has better background behavior than MPC but is still restricted:

This means Network.framework is appropriate for bulk data transfer after a contact has been identified (via iBeacon monitoring or CoreBluetooth), but cannot serve as the passive background discovery layer.

Privacy Consideration: mDNS Service Names

When you advertise a service via NWListener.Service, the service name is broadcast over mDNS and is visible to any device on the network (or via AWDL peer-to-peer). If you use a persistent device identifier as the service name, this creates a tracking vector.

The wrong approach:

// BAD: stable device identifier in mDNS name
listener?.service = NWListener.Service(
    name: "user-alice-device-abc123",
    type: "_yourapp._tcp"
)

The right approach:

// GOOD: rotating opaque name derived from a time-windowed commitment
let opaqueEpoch = currentTimeEpoch()  // changes every 15 minutes
let rotatingName = HMAC-SHA256(
    key: devicePrivateSecret,
    data: "mDNS-name-\(opaqueEpoch)"
).prefix(16).hexString

listener?.service = NWListener.Service(
    name: rotatingName,
    type: "_yourapp._tcp"
)

The type field (_yourapp._tcp) is not secret — it identifies the app. But the name field can be opaque and rotating, preventing device tracking via mDNS.

Cross-Platform Potential

mDNS (Bonjour) is an open standard (RFC 6762, RFC 6763). Android supports mDNS via NsdManager (Android 4.1+). This means that in theory, a device advertising via NWListener on iOS can be discovered by an Android device using NsdManager.discoverServices().

In practice, cross-platform mDNS over peer-to-peer WiFi (AWDL) has limitations:

Realistic cross-platform scenario: Both devices on the same WiFi network → mDNS cross-platform discovery works. Over AWDL (device-to-device, no WiFi network) → iOS-only.

For our offline-first use case (no internet required), cross-platform Network.framework discovery over AWDL is not viable. CoreBluetooth GATT remains the cross-platform discovery mechanism.

Comparison: Network.framework vs. BLE for This Use Case

Consideration CoreBluetooth BLE Network.framework P2P
Passive background discovery Possible (limited) Not possible for new connections
Cross-platform Yes iOS-only (AWDL) or same-network WiFi
Initial contact detection Yes (beacon/GATT) No
Bulk data sync after contact Limited (~1 Mbps theoretical) Yes (WiFi speeds)
Power consumption Low (BLE designed for this) Higher (WiFi radio)
Range 10–100m 10–100m (AWDL, similar to WiFi)
Protocol control Full Full

Conclusion: Use CoreBluetooth for discovery and initial contact identification. Use Network.framework P2P (or WiFi Direct on Android) for bulk message/data sync once contact has been established.


9. Practical Recommendations

For the Prototype Phase

Should this app be Android-first?

Yes. The recommendation is to build and validate the core protocol on Android first, for the following reasons:

  1. Android has no meaningful background BLE restrictions. A foreground service can scan and advertise continuously and indefinitely. This means you can test the actual protocol logic — key exchange, contact recognition, message sync — without fighting the platform.

  2. The iOS BLE constraints are a delivery risk, not a design risk. The protocol design itself (BLE GATT for discovery, cryptographic identity, offline-first sync) is sound. iOS's constraints affect how reliably the protocol can operate on that platform, not whether the protocol design is correct.

  3. An Android prototype proves the concept. A working Android-to-Android proximity contact exchange demonstrates the product to stakeholders, validates the UX, and gives you a reference implementation to port from.

  4. iOS work is specialized and slow. Working around iOS BLE constraints (state restoration, iBeacon integration, permission flows) is iOS-specific engineering that adds significant complexity. Do this after the core protocol is stable.

Minimum viable iOS support: What can realistically be promised?

The honest user-facing statement: "Background contact detection works on iOS when you have granted location permission. Detection may take 30–60 seconds. App must not be force-quit by the user."

Which Workaround to Use

Primary recommendation: iBeacon monitoring + state restoration

Use both in combination:

The iBeacon approach requires "Always" location permission. This is the cost of reliable iOS background detection. Do not hide this from users — explain it clearly.

If location permission is unacceptable: State restoration with CoreBluetooth background modes only. Expect degraded reliability. Background detection will not work after app termination. Detection latency will be higher. This is a worse user experience but avoids the location permission requirement.

Do not rely on Multipeer Connectivity: The iOS 18 regression makes it unreliable at typical social distances. Until Apple resolves this, MPC should not be in the critical path.

Permission Strategy

Step 1: Bluetooth permission (required, no workaround)

Show a pre-permission dialog before the system prompt:

"This app uses Bluetooth to detect when contacts you've met in person are nearby. Tap Continue to grant Bluetooth access."

Then request: CBCentralManager initialization triggers the Bluetooth system prompt.

Step 2: Location permission (required for iBeacon background detection)

Do not ask for location permission at launch. Ask only when the user explicitly enables "background contact detection" in settings:

Pre-permission dialog:

"To detect nearby contacts when the app is in the background, iOS requires Location permission. This app uses only Bluetooth proximity detection — no GPS data is ever collected or stored. Tap Enable to grant this permission."

Then call requestAlwaysAuthorization().

If user declines location permission: The app still works in the foreground. Show a persistent settings banner: "Background detection is disabled. Grant Location permission in Settings to enable it."

"Reduced functionality on iOS" mode: Yes, implement this as a first-class concept. The app should have a settings toggle "Background contact detection" that is labeled "Requires Location permission on iOS." Users who are privacy-sensitive and do not want to grant location permission should get a clean foreground-only experience, not a broken app.

Testing Methodology

Devices required for comprehensive testing:

Critical test scenarios (test each systematically):

Scenario Expected behavior How to test
Both devices in foreground Immediate mutual discovery Standard use
One device backgrounded iBeacon monitoring: 5–30s detection. CoreBluetooth: delivery with latency Send backgrounded device to home screen, start timer
Both devices backgrounded iBeacon monitoring on both: ~30s detection Both home screen, verify callbacks via debug log
Backgrounded + screen locked Same as backgrounded Lock screen, verify callbacks
App terminated by system iBeacon: relaunch + detect. CB: may not work Simulate memory pressure or use Xcode to terminate
App force-quit by user iBeacon: does NOT work (OS disable). CB: does not work Swipe app out of switcher; verify no detection
Low battery mode All callbacks delayed further Enable Low Power Mode in settings
iPhone 17 regression Background CB scanning stops; iBeacon still works Test on iPhone 17 with iOS 18.x

Tools:

Logging pattern for background testing:

import os.log

private let bleLog = Logger(subsystem: "com.yourapp", category: "BLE")

func centralManager(
    _ central: CBCentralManager,
    didDiscover peripheral: CBPeripheral,
    advertisementData: [String: Any],
    rssi RSSI: NSNumber
) {
    bleLog.info(
        "didDiscover: \(peripheral.identifier) rssi=\(RSSI) " +
        "appState=\(UIApplication.shared.applicationState.rawValue)"
    )
}

The applicationState in the log helps distinguish foreground vs. background callbacks when reviewing logs after a test run.


10. Summary Decision Matrix

Scenario Recommended Mechanism Background Works? Cross-Platform? Location Perm Needed? Notes
iOS-to-iOS passive discovery (background) iBeacon monitoring + CoreBluetooth state restoration Yes — iBeacon is reliable; CB is supplementary No (iOS only) Yes (Always) iBeacon is the reliable trigger; CB delivers detail
iOS-to-Android passive discovery (background) CoreBluetooth GATT + iBeacon as wake trigger Unreliable — iOS peripheral invisible to Android when backgrounded Yes (when iOS is foreground or Android is aggressive scanner) Yes (for iBeacon wake) Android must be the scanner; iOS cannot advertise to Android while backgrounded
Initial contact exchange (foreground, first meeting) CoreBluetooth GATT + NFC tap or QR code N/A (foreground) Yes No NFC/QR for key exchange; BLE for subsequent recognition
Known contact detected nearby (foreground) CoreBluetooth scanning N/A (foreground) Yes No Full functionality in foreground on all platforms
Known contact detected nearby (background, iOS) iBeacon monitoring → relaunch → CoreBluetooth connect Yes (iBeacon reliable, 5–30s latency) No (iBeacon is iOS advertising) Yes Best available iOS background detection
Bulk data sync after contact detected Network.framework P2P WiFi (iOS-iOS) or WiFi Direct (cross-platform) Limited (no new connections in background) Partial (AWDL iOS-iOS; WiFi Direct cross-platform) No Initiate while in foreground; keep session alive if background needed
Message sync with known contact in range GATT notify on established connection Yes (notify works in background with bluetooth-central) Yes No Maintain GATT connection; notify is the most reliable background data path
iOS 17.x and earlier — general background CoreBluetooth background modes + state restoration Reasonably reliable Yes (central scanning) No (but iBeacon adds reliability) iOS 17 is the most stable baseline
iPhone 17 device (2026 regression) iBeacon monitoring only Yes for iBeacon; No for CoreBluetooth background scan No (iBeacon only, iOS advertising) Yes CoreBluetooth background scanning broken on this hardware
Low battery / power saving mode iBeacon monitoring (most power-efficient background mechanism) Yes (OS handles it efficiently) No Yes BLE GATT scanning degrades further in low power mode; iBeacon less affected

Appendix: Key References and Source Material

The following sources informed this document. Where specific behaviors are claimed, these are the primary evidence sources:


Last updated: April 2026. iOS 26 behavior documented below.


Updates (2025–2026)

iPhone 17 BLE Background Scanning: Still Unresolved

The iPhone 17 hardware regression documented in this page remains unresolved as of early 2026. An Apple engineer (WWDR Engineering / Core Technologies) acknowledged the issue in February 2026, requested sysdiagnose logs and Bluetooth diagnostic profiles, and confirmed that bug report FB20381425 remains under investigation. No software fix has been released through iOS 26 as of April 2026. Source: Apple Developer Forums — iPhone 17 bluetooth background scanning issue — primary thread documenting the regression and Apple's response.

Apple Formally Deprecates MultipeerConnectivity (March 2025)

In March 2025 Apple published a detailed migration guide titled "Moving from Multipeer Connectivity to Network Framework." This formalises what was signalled with iOS 18: MPC is functionally deprecated. Apple engineers enumerate eight structural deficiencies — poor throughput, no flow control, forced P2P Wi-Fi even when unwanted, PKI-only security, and known unfixed bugs. The guide recommends Network.framework as the active replacement, offering QUIC, WebSocket, TCP, and UDP transports, TLS-PSK (simpler to deploy in peer-to-peer contexts), opt-in P2P Wi-Fi, and Swift-first concurrency APIs. Source: Moving from Multipeer Connectivity to Network Framework — Apple Developer Forums.

Design implication: Any iOS proximity transport layer built on MPC should be treated as a temporary measure. The ~2 m connection range regression on iOS 18 MPC is an additional reason to accelerate migration to Network.framework for bulk transfer.

WWDC 2025: Wi-Fi Aware Framework (iOS 26)

Contradicts existing content: The page notes "iOS 19 (2026, projected) — No public beta at time of writing." iOS 19 was branded iOS 26 and introduced a native Wi-Fi Aware framework as the most significant proximity networking announcement at WWDC 2025.

Wi-Fi Aware (Wi-Fi Alliance NAN standard) enables direct device-to-device communication without a router, while simultaneously maintaining an active internet Wi-Fi connection. Key properties:

Sources: WWDC25 Session 228 — Supercharge device connectivity with Wi-Fi Aware, Apple Developer Forums — Wi-Fi Aware between iOS 26 and Android.

Design implication: Wi-Fi Aware is a strong future transport candidate for iOS-to-iOS bulk data sync once the install base reaches iPhone 12+ saturation. It does not solve the Android interoperability problem in the short term. BLE must remain the universal discovery layer; Wi-Fi Aware can serve as a high-throughput upgrade path for iOS peers that have completed a BLE-based handshake.

Network.framework: Swift Structured Concurrency APIs (iOS 26)

WWDC 2025 Session 250 introduced async/await-native connection and listener management for Network.framework. This reduces the boilerplate required for the BLE-to-Network.framework handoff pattern and makes state restoration integration cleaner. Source: WWDC25 Session 250 — Use structured concurrency with Network framework.

Berty / Wesh: Proximity Transport Stability Fix (January 2025)

Berty's v2.470.9 release (January 22, 2025) fixed a critical bug where IPFS stalled when proximity transports (BLE and MPC) were enabled simultaneously. This confirms that the three-transport strategy (MPC for iOS-iOS, GATT for iOS-Android, internet fallback) remains the active architecture in Wesh-based implementations, and that the interaction between BLE event loops and IPFS's content routing is a known fragility. Source: Berty GitHub Releases.