iOS SBeacon Framework Beacon Scanning - scopetech/Mobile-Public GitHub Wiki
iOS SBeacon Framework v1.0.8
This guide covers discovering unpaired SBeacon devices using BLE scanning.
Beacon scanning allows you to discover nearby unpaired SBeacon devices. Discovered beacons appear in SBeaconManager.shared.unpairedSBeacons array and can be paired to your account.
- Installation completed
- Initialization configured
- Bluetooth permission granted (happens automatically on first scan)
import BLEFramework
// Start scanning for SBeacon devices
SBeaconManager.shared.startScan()
// Scanning continues until you call stopScan()For battery efficiency, scan for a limited duration:
func scanForBeacons(duration: TimeInterval = 10.0) {
SBeaconManager.shared.startScan()
DispatchQueue.main.asyncAfter(deadline: .now() + duration) {
SBeaconManager.shared.stopScan()
}
}The framework provides a convenience method for 10-second scans:
// Scans for 10 seconds then automatically stops
SBeaconManager.shared.scanForBeacons()// Stop scanning to save battery
SBeaconManager.shared.stopScan()Important: Always stop scanning when:
- You've found the beacon you're looking for
- User navigates away from the scanning screen
- App enters background
- Battery optimization is important
Unpaired beacons discovered during scanning:
public class UnpairedSBeacon {
public var minor: NSNumber // iBeacon minor ID
public var major: NSNumber // iBeacon major ID
public var macAddressEnding: String // Last 4 bytes of MAC (e.g., "a3b5c7d9")
public var peripheral: CBPeripheral? // BLE peripheral reference
public var lastTimeUpdated: Date // Last time beacon was seen
public var uuid: UUID // SBeacon UUID (E2C56DB5-DFFB...)
public var range: NSNumber? // RSSI signal strength
}// Get all discovered unpaired beacons
let unpairedBeacons = SBeaconManager.shared.unpairedSBeacons
for beacon in unpairedBeacons {
print("Found beacon: ****\(beacon.macAddressEnding)")
print(" Major: \(beacon.major)")
print(" Minor: \(beacon.minor)")
print(" RSSI: \(beacon.range ?? -100)")
}func findBeacon(major: Int, minor: Int) -> UnpairedSBeacon? {
return SBeaconManager.shared.unpairedSBeacons.first { beacon in
beacon.major.intValue == major && beacon.minor.intValue == minor
}
}// Get beacons with strong signal (closer devices)
let nearbyBeacons = SBeaconManager.shared.unpairedSBeacons.filter { beacon in
guard let rssi = beacon.range?.intValue else { return false }
return rssi > -70 // Strong signal threshold
}// Get beacons seen in last 10 seconds
let recentBeacons = SBeaconManager.shared.unpairedSBeacons.filter { beacon in
let timeSinceUpdate = Date().timeIntervalSince(beacon.lastTimeUpdated)
return timeSinceUpdate < 10.0
}import SwiftUI
import BLEFramework
struct BeaconScanView: View {
@StateObject private var viewModel = BeaconScanViewModel()
var body: some View {
List {
Section("Unpaired Beacons") {
ForEach(viewModel.unpairedBeacons, id: \.macAddressEnding) { beacon in
BeaconRow(beacon: beacon)
.onTapGesture {
viewModel.pairBeacon(beacon)
}
}
if viewModel.unpairedBeacons.isEmpty {
Text("No beacons found")
.foregroundColor(.secondary)
}
}
}
.navigationTitle("Scan for Beacons")
.refreshable {
await viewModel.refresh()
}
.onAppear {
viewModel.startScanning()
}
.onDisappear {
viewModel.stopScanning()
}
}
}
struct BeaconRow: View {
let beacon: UnpairedSBeacon
var body: some View {
HStack {
VStack(alignment: .leading) {
Text("****\(beacon.macAddressEnding)")
.font(.headline)
Text("Major: \(beacon.major), Minor: \(beacon.minor)")
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
// Signal strength indicator
Image(systemName: signalStrengthIcon)
.foregroundColor(signalStrengthColor)
}
}
private var signalStrengthIcon: String {
guard let rssi = beacon.range?.intValue else {
return "antenna.radiowaves.left.and.right.slash"
}
if rssi > -60 { return "antenna.radiowaves.left.and.right" }
if rssi > -75 { return "antenna.radiowaves.left.and.right.slash" }
return "wifi.slash"
}
private var signalStrengthColor: Color {
guard let rssi = beacon.range?.intValue else { return .gray }
if rssi > -60 { return .green }
if rssi > -75 { return .orange }
return .red
}
}import Foundation
import Combine
import BLEFramework
class BeaconScanViewModel: ObservableObject {
@Published var unpairedBeacons: [UnpairedSBeacon] = []
@Published var isScanning = false
private var refreshTimer: Timer?
private let refreshInterval: TimeInterval = 1.0
deinit {
stopScanning()
}
func startScanning() {
SBeaconManager.shared.startScan()
isScanning = true
// Periodically refresh the list to show updated RSSI values
refreshTimer = Timer.scheduledTimer(withTimeInterval: refreshInterval, repeats: true) { [weak self] _ in
self?.refreshBeacons()
}
// Save battery by allowing timer tolerance
refreshTimer?.tolerance = refreshInterval / 2
}
func stopScanning() {
SBeaconManager.shared.stopScan()
refreshTimer?.invalidate()
refreshTimer = nil
isScanning = false
}
func refresh() async {
// For pull-to-refresh
startScanning()
try? await Task.sleep(nanoseconds: 5_000_000_000) // 5 seconds
refreshBeacons()
}
func pairBeacon(_ beacon: UnpairedSBeacon) {
// Pairing logic (see Beacon Pairing guide)
}
private func refreshBeacons() {
// Copy array to trigger @Published update
unpairedBeacons = SBeaconManager.shared.unpairedSBeacons
}
}import UIKit
import BLEFramework
class BeaconScanViewController: UITableViewController {
private var unpairedBeacons: [UnpairedSBeacon] = []
private var refreshTimer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
title = "Scan for Beacons"
setupRefreshControl()
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
startScanning()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
stopScanning()
}
// MARK: - Scanning
private func startScanning() {
SBeaconManager.shared.startScan()
// Refresh list every second
refreshTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.refreshBeacons()
}
}
private func stopScanning() {
SBeaconManager.shared.stopScan()
refreshTimer?.invalidate()
refreshTimer = nil
}
private func refreshBeacons() {
unpairedBeacons = SBeaconManager.shared.unpairedSBeacons
tableView.reloadData()
}
private func setupRefreshControl() {
refreshControl = UIRefreshControl()
refreshControl?.addTarget(self, action: #selector(handleRefresh), for: .valueChanged)
}
@objc private func handleRefresh() {
refreshBeacons()
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in
self?.refreshControl?.endRefreshing()
}
}
// MARK: - Table View
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return unpairedBeacons.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "BeaconCell") ?? UITableViewCell(style: .subtitle, reuseIdentifier: "BeaconCell")
let beacon = unpairedBeacons[indexPath.row]
cell.textLabel?.text = "****\(beacon.macAddressEnding)"
cell.detailTextLabel?.text = "Major: \(beacon.major), Minor: \(beacon.minor)"
// Show signal strength
if let rssi = beacon.range?.intValue {
cell.accessoryView = signalStrengthView(for: rssi)
}
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let beacon = unpairedBeacons[indexPath.row]
pairBeacon(beacon)
}
private func signalStrengthView(for rssi: Int) -> UIView {
let label = UILabel()
label.font = .systemFont(ofSize: 12)
if rssi > -60 {
label.text = "Strong"
label.textColor = .systemGreen
} else if rssi > -75 {
label.text = "Medium"
label.textColor = .systemOrange
} else {
label.text = "Weak"
label.textColor = .systemRed
}
label.sizeToFit()
return label
}
private func pairBeacon(_ beacon: UnpairedSBeacon) {
// Pairing logic (see Beacon Pairing guide)
}
}- Scan Start → Framework subscribes to BLE scanner
-
Device Discovered → Added to
unpairedSBeaconsarray -
RSSI Updates →
rangeproperty updated every ~1 second - Device Timeout → Beacon removed if not seen for 20 seconds
- Scan Stop → Framework unsubscribes from BLE scanner
| RSSI Range | Distance | Signal Quality |
|---|---|---|
| -50 to 0 | < 1 meter | Excellent |
| -60 to -51 | 1-3 meters | Good |
| -75 to -61 | 3-10 meters | Fair |
| -90 to -76 | 10-30 meters | Poor |
| < -90 | > 30 meters | Very Poor |
All SBeacon devices use the same UUID:
E2C56DB5-DFFB-48D2-B060-D0F5A71096E0
Devices are uniquely identified by their major and minor values.
1. Stop Scanning When Not Needed
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
SBeaconManager.shared.stopScan()
}2. Use Timed Scans
// Scan for 10 seconds, then stop
SBeaconManager.shared.scanForBeacons() // Auto-stops after 10s3. Limit Refresh Rate
// Don't refresh UI more than once per second
refreshTimer?.tolerance = 0.5 // Allow system to optimize4. Scan Only When Necessary
- Don't scan continuously in background
- Pause scanning during other intensive operations
- Consider user-initiated scanning (pull-to-refresh)
Problem: unpairedSBeacons array is empty
Solutions:
- Ensure Bluetooth is enabled on device
- Check Bluetooth permission granted (should prompt automatically)
- Verify SBeacon device is powered on and advertising
- Move closer to beacon (< 10 meters)
- Check beacon battery level
Problem: Beacons appear then vanish
Cause: Beacons not seen for 20 seconds are automatically removed
Solutions:
- Ensure beacon is actively advertising
- Check beacon battery isn't depleted
- Reduce distance to beacon
- Check for RF interference
Problem: range property stays constant
Solutions:
- Ensure you're refreshing the UI periodically
- Check scanning is actually running (
scanRequestCounter > 0) - Verify beacon is in range
Problem: Same beacon appears multiple times
Cause: Framework should handle this automatically, but check:
Solutions:
- Framework uses major/minor for uniqueness
- Check you're not creating multiple managers
- Ensure proper singleton usage
-
Start/Stop Pattern: Always pair
startScan()withstopScan() -
Lifecycle Management: Stop scanning in
viewWillDisappear/.onDisappear -
Weak References: Use
[weak self]in timers and closures - Battery Awareness: Limit continuous scanning duration
- User Feedback: Show scanning indicator in UI
- Empty States: Handle case when no beacons found
- Signal Strength: Display RSSI to help users locate beacon
- Timeout Handling: Beacons disappear after 20s automatically
- Beacon Pairing - Pair discovered beacons
- Initialization - Setup before scanning
- Data Models - UnpairedSBeacon reference