You’ve set up your Core Bluetooth scanning code. You hit run. Your peripheral is powered on and supposedly advertising. But when you check your scan results… nothing. An empty array. Radio silence.
If you’re reading this, you’re probably frustrated and under pressure to get this working. The good news? You’re not alone, and this is almost always fixable. iOS BLE scanning has some unique constraints and behaviors that differ significantly from Android, and they can trip up even experienced iOS developers who are newer to Bluetooth.
This guide will walk you through a structured approach to diagnose and fix empty scan results, starting with quick wins and moving into iOS-specific quirks that commonly cause this issue.
Start With the Basics: Quick Checks
Before diving into the nuances of iOS scanning behavior, let’s rule out the most common culprits. These quick checks solve the problem more often than you’d think.
Verify Bluetooth State and Permissions
Your app needs proper permissions to use Bluetooth. Make sure your Info.plist includes:
Privacy - Bluetooth Always Usage Description: Required for iOS 13 and later. Without this key, your app will crash on launch.- A clear, honest description of why your app uses Bluetooth (e.g., “This app uses Bluetooth to connect to your fitness tracker”).
Your app also needs Bluetooth to be powered on. The most common mistake here is trying to start a scan before CoreBluetooth is ready. Your CBCentralManagerDelegate must implement centralManagerDidUpdateState(_:), and you should only start scanning after receiving a .poweredOn state:
extension BluetoothViewController: CBCentralManagerDelegate {
func centralManagerDidUpdateState(_ central: CBCentralManager) {
switch central.state {
case .poweredOn:
startScan()
case .poweredOff:
// Alert user to turn on Bluetooth
case .resetting:
// Wait for next state update and consider logging interruption of Bluetooth service
case .unauthorized:
// Alert user to enable Bluetooth permission in app Settings
case .unsupported:
// Alert user their device does not support Bluetooth and app will not work as expected
case .unknown:
// Wait for next state update
}
}
}If you’re calling scanForPeripherals immediately after initializing your CBCentralManager, you’ll likely see a warning in the Xcode console and get no results. The state update happens asynchronously, usually within milliseconds, but you must wait for it.
Restart Bluetooth and Your Device
It sounds almost too simple, but Bluetooth state can get stuck. Toggle Bluetooth off and on in Control Center, or restart your iPhone entirely. This clears temporary state issues that can prevent scanning from working properly.
Confirm Your Peripheral Is Actually Advertising
This is critical: is your peripheral actually broadcasting? You can’t debug iOS scanning behavior if the device isn’t advertising in the first place.
Use LightBlue (our free BLE scanner app) to verify your peripheral shows up on iOS. If LightBlue can’t see it, the problem is with your peripheral’s advertising configuration, not your app. LightBlue gives you a ground truth: if iOS can see the device at all, LightBlue will show it.
Remove Service Filters Temporarily
If you’re filtering your scan with specific service UUIDs, try running a completely open scan first:
func startScan() {
centralManager.scanForPeripherals(withServices: nil, options: nil)
}If your peripheral suddenly appears, the issue is your filter. Double-check that:
- The service UUID you’re filtering for matches exactly what your peripheral advertises.
- Your peripheral is actually advertising that service (many devices only advertise a subset of their services).
Structured Debugging: Narrowing Down the Problem
If the quick checks didn’t solve it, let’s systematically isolate where the failure is happening. The issue exists at one of three layers:
1. Peripheral Layer
Is the peripheral advertising correctly?
- Advertisement interval: If your peripheral advertises too infrequently (e.g., every few seconds), iOS might miss it during short scanning windows, especially in the background.
- Advertisement format: iOS expects standard BLE advertisement formats. Non-compliant packets may be silently ignored.
- Transmit power: Very weak transmit power can make your device invisible beyond a few inches.
If LightBlue sees your device consistently but your app doesn’t, the peripheral probably isn’t the problem. If LightBlue only sees it intermittently or not at all, investigate your peripheral’s firmware and advertising configuration.
2. App/Code Layer
Is your scanning code set up correctly? Here are the most common issues:
State Management
Scanning must start only after centralManagerDidUpdateState returns .poweredOn. This is the most common culprit for empty scan results. Add logging to your state handler to confirm you’re actually reaching the .poweredOn case and that your scan is starting. Check your Xcode console for any Core Bluetooth warnings.
Delegate Implementation
Verify that centralManager(_:didDiscover:advertisementData:rssi:) is actually implemented in your CBCentralManagerDelegate. This is where scan results are delivered. If this method isn’t being called, either your scan isn’t running or no peripherals match your filters.
Delegate Assignment
Confirm you’ve actually set the delegate on your CBCentralManager when you initialize it. A common oversight is forgetting to set delegate: self in the initializer.
Strong References
Once you discover a peripheral, you must keep a strong reference to it. From the Ultimate Guide: “Once the scan callback returns a CBPeripheral object, you must retain a strong reference to it in your code. If you simply call connect immediately from the didDiscover delegate method and let that function block complete without strongly storing the peripheral elsewhere, the peripheral object will be deallocated and any connection or pending connection broken.” Store discovered peripherals in a property or array immediately upon discovery.
3. Platform Layer
Is iOS restricting or filtering results?
If your peripheral is advertising correctly and your code looks right, you’re likely running into iOS-specific scanning restrictions. These platform-level behaviors are different from Android and can silently filter out results.
iOS-Specific Factors That Cause Empty Scan Results
iOS treats BLE scanning differently than Android, with stricter power management, privacy protections, and filtering rules. Understanding these behaviors is essential for debugging scan failures.
Background Scanning Limitations
iOS aggressively throttles scanning when your app isn’t in the foreground. Background scans:
- Happen much less frequently (every few seconds rather than continuously).
- Only return results for peripherals advertising services you’ve explicitly filtered for.
- Require the “Background Modes” capability with “Uses Bluetooth LE accessories” enabled in your
Info.plist.
The fix: Test scanning in the foreground first. If your scan works in the foreground but fails in the background, you’re hitting iOS’s background restrictions. You’ll need to filter by specific service UUIDs when scanning in the background, and your peripheral must advertise those services.
Advertisement Filtering and Format
iOS is picky about which advertisements it surfaces:
- Service UUID filtering: When you filter by service UUIDs, iOS only shows peripherals advertising those exact services. If your peripheral advertises different services or none at all, it won’t appear.
- Advertisement packet format: iOS expects standard-compliant BLE advertisement packets. Malformed or non-standard packets may be ignored without error.
- Advertisement interval: Peripherals that advertise infrequently (e.g., every 2+ seconds) are more likely to be missed, especially during short scanning windows or when the app is backgrounded.
The fix: Use an open scan (withServices: nil) in the foreground to establish baseline visibility. If that works, progressively add filters to identify what’s being filtered out.
Scan Timing and Advertisement Windows
If you start your scan after your peripheral has already sent its advertisement packets, you’ll miss them until the next advertising event. This is especially problematic for peripherals with long advertising intervals.
Additionally, iOS caches scan results. By default, scanForPeripherals only calls your delegate once per peripheral per scan session. If you need updated RSSI values or want to catch every advertisement packet, you can enable duplicate filtering using the CBCentralManagerScanOptionAllowDuplicatesKey option. However, Apple recommends keeping this option off when possible, as it uses much more power and memory than the default behavior.
Permission State
If your app is in an .unauthorized state, scanning may fail silently or return no results. Always check the state in centralManagerDidUpdateState and handle .unauthorized explicitly by directing users to manually grant Bluetooth permissions in Settings. The state cases you need to handle are: .poweredOn, .poweredOff, .resetting, .unauthorized, .unsupported, and .unknown.
Cached Peripheral Behavior
iOS maintains an internal cache of previously connected peripherals. If you’ve bonded with a peripheral before, iOS may automatically reconnect to it at the system level without your app being aware. This can complicate discovery. You might need to use retrieveConnectedPeripherals(withServices:) or retrievePeripherals(withIdentifiers:) to access peripherals iOS already knows about.
No Access to MAC Addresses
Unlike Android, iOS doesn’t expose peripheral MAC addresses. Instead, it assigns each peripheral a UUID that’s unique to your app but not guaranteed to persist across sessions. If you’re trying to identify or filter for a specific peripheral based on its MAC address, that approach won’t work on iOS. Instead, use advertisement data (like manufacturer data or service data) or the peripheral’s name for identification.
Practical Fixes and Best Practices
Based on the factors above, here’s a checklist of fixes and best practices to prevent empty scan results:
1. Always wait for .poweredOn before scanning
Never call scanForPeripherals outside of the .poweredOn case in centralManagerDidUpdateState.
2. Start with an open scan in the foreground
Use scanForPeripherals(withServices: nil) with no filters to confirm basic visibility. Once that works, add service filters incrementally.
3. Verify advertisement format and interval
Work with your firmware team to ensure the peripheral advertises frequently (ideally every 100-500ms) and uses standard BLE advertisement formats that comply with Bluetooth SIG specifications.
4. Keep strong references to discovered peripherals
Store peripherals in an array or dictionary immediately upon discovery. The central manager does not internally retain strong connections to discovered peripherals, so your app must do this explicitly.
5. Handle background scanning separately
Don’t expect background scanning to work the same as foreground scanning. Filter by specific services, add the background mode capability to your Info.plist, and test background behavior independently after you’ve confirmed foreground scanning works.
6. Use LightBlue to validate peripheral behavior
When in doubt, LightBlue is your ground truth. If it can’t see your peripheral consistently, fix the peripheral before debugging your app further.
7. Log everything during debugging
Add console logging to centralManagerDidUpdateState to verify you’re reaching .poweredOn, and to centralManager(_:didDiscover:advertisementData:rssi:) to confirm when peripherals are discovered. Check your Xcode console for CoreBluetooth warnings that might indicate what’s going wrong.
8. Test with minimal code first
If you’re still stuck, create a bare-bones test implementation with just the essential CoreBluetooth setup. Sometimes the issue is interference from other parts of your app.
When It’s Still Not Working
If you’ve worked through all of the above and still see no results, here are some final troubleshooting steps:
Test with a second iOS device: Bluetooth behavior can vary slightly by device and iOS version. If scanning works on a different iPhone or iPad, you may be dealing with a device-specific issue.
Strip your code down to a minimal implementation: Create a new test project with the absolute minimum CoreBluetooth code—just a CBCentralManager, a delegate, and a call to scanForPeripherals. If this works but your main app doesn’t, something else in your app is interfering.
Check for iOS version-specific bugs: While rare, iOS updates occasionally introduce BLE regressions. Search for known issues related to your iOS version and CoreBluetooth.
Verify peripheral firmware compliance: If your peripheral is custom hardware, have your firmware team verify that the advertisement packets conform to the Bluetooth SIG specification. Non-compliant packets may be rejected by iOS without any error message to your app.
Consider using a BLE sniffer: Tools like the Nordic nRF Sniffer can capture raw BLE packets and show you exactly what’s being broadcast. This is invaluable for confirming that your peripheral is advertising as expected and that iOS is actually receiving the packets.
Wrapping Up
Empty scan results on iOS are frustrating, but they’re almost always caused by one of a few common issues:
- Scanning before Bluetooth is ready (not waiting for
.poweredOn) - Missing or incorrect permissions in
Info.plist - Overly restrictive service filters
- Peripheral not advertising, or advertising incorrectly
- iOS-specific scanning restrictions (especially in background mode)
The key is to approach the problem systematically: start with the basics, use tools like LightBlue to establish ground truth, and then progressively narrow down whether the issue is with your peripheral, your code, or iOS platform behavior.
Once you’ve got scanning working reliably, consider exploring our Ultimate Guide to iOS Core Bluetooth for deeper insights into connecting, reading, writing, and managing BLE peripherals on iOS. And if you’re building a connected product and need help architecting your BLE solution from the ground up, we’d love to talk.
The most important thing? Don’t give up. BLE development has a learning curve, but once you understand iOS’s quirks, you’ll know exactly how to debug issues like this in minutes instead of hours.




