๐ถ Core Bluetooth
โญ Difficulty: โญโญโญโญ
โฑ๏ธ Est. Time: 3-4h
๐ System & Network
BLE device scanning, connection, and data communication
iOS 5+BLE 5.0
โจ Core Bluetooth?
Core Bluetooth is a framework that uses the Bluetooth Low Energy (BLE) protocol to scan, connect, and exchange data with nearby devices. You can communicate with smartwatches, fitness trackers, sensors, and IoT devices, reading and writing data through GATT services and characteristics. It operates on the Central-Peripheral architecture.
๐ก Key Features: BLE Device Scanning ยท Peripheral Connection ยท GATT Service Discovery ยท Characteristic Read/Write ยท Notification Subscription ยท Battery-Efficient Communication ยท Background Operation
๐ 1. CBCentralManager Initialization
Create a manager that scans and connects BLE devices as a Central role.
BluetoothManager.swift โ Central Manager Initialization
import CoreBluetooth import SwiftUI @Observable class BluetoothManager: NSObject { private var centralManager: CBCentralManager! var discoveredPeripherals: [CBPeripheral] = [] var connectedPeripheral: CBPeripheral? var isBluetoothOn = false var isScanning = false override init() { super.init() // Central Manager ์์ฑ centralManager = CBCentralManager(delegate: self, queue: nil) } // Start scanning func startScanning() { guard isBluetoothOn else { print("โ ๏ธ Bluetooth๊ฐ ๊บผ์ ธ ์์ต๋๋ค") return } discoveredPeripherals.removeAll() isScanning = true // all BLE ๊ธฐ๊ธฐ ์ค์บ (์๋น์ค ํํฐ ์์) centralManager.scanForPeripherals( withServices: nil, options: [CBCentralManagerScanOptionAllowDuplicatesKey: false] ) print("๐ BLE ๊ธฐ๊ธฐ Start scanning") } // ํน์ ์๋น์ค๋ง ์ค์บ func scanForDevices(withServices serviceUUIDs: [CBUUID]) { guard isBluetoothOn else { return } discoveredPeripherals.removeAll() isScanning = true centralManager.scanForPeripherals(withServices: serviceUUIDs, options: nil) print("๐ ํน์ ์๋น์ค ์ค์บ: \(serviceUUIDs)") } // Stop scanning func stopScanning() { centralManager.stopScan() isScanning = false print("โน๏ธ Stop scanning") } // Peripheral ์ฐ๊ฒฐ func connect(peripheral: CBPeripheral) { stopScanning() centralManager.connect(peripheral, options: nil) print("๐ ์ฐ๊ฒฐ ์๋: \(peripheral.name ?? "Unknown")") } // Connect ํด์ func disconnect() { guard let peripheral = connectedPeripheral else { return } centralManager.cancelPeripheralConnection(peripheral) print("โ ์ฐ๊ฒฐ ํด์ ") } }
๐ก 2. Central Manager Delegate
Handle Bluetooth state changes and device discovery.
BluetoothManager+Delegate.swift โ Central Delegate
import CoreBluetooth extension BluetoothManager: CBCentralManagerDelegate { // Bluetooth state change func centralManagerDidUpdateState(_ central: CBCentralManager) { switch central.state { case .poweredOn: print("โ Bluetooth ์ผ์ง") isBluetoothOn = true case .poweredOff: print("โ Bluetooth ๊บผ์ง") isBluetoothOn = false case .unauthorized: print("โ ๏ธ Bluetooth ๊ถํ ์์") isBluetoothOn = false case .unsupported: print("โ Bluetooth Not supported") isBluetoothOn = false case .resetting: print("๐ Bluetooth ์ฌ์ค์ ์ค") case .unknown: print("โ Bluetooth ์ํ ๋ถ๋ช ") @unknown default: break } } // Peripheral ๋ฐ๊ฒฌ func centralManager( _ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber ) { // ์ด๋ฏธ ๋ฐ๊ฒฌ๋ ๊ธฐ๊ธฐ๋ ์ถ๊ฐํ์ง ์์ guard !discoveredPeripherals.contains(where: { $0.identifier == peripheral.identifier }) else { return } discoveredPeripherals.append(peripheral) let deviceName = peripheral.name ?? "Unknown Device" let signalStrength = RSSI.intValue print("๐ถ ๋ฐ๊ฒฌ: \(deviceName) (RSSI: \(signalStrength)dBm)") // Advertisement Data ์ถ๋ ฅ if let localName = advertisementData[CBAdvertisementDataLocalNameKey] as? String { print(" Local Name: \(localName)") } if let serviceUUIDs = advertisementData[CBAdvertisementDataServiceUUIDsKey] as? [CBUUID] { print(" Services: \(serviceUUIDs)") } } // Connection successful func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { print("โ Connection successful: \(peripheral.name ?? "Unknown")") connectedPeripheral = peripheral peripheral.delegate = self // ์๋น์ค ํ์ peripheral.discoverServices(nil) } // Connection failed func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) { print("โ Connection failed: \(error?.localizedDescription ?? "Unknown error")") connectedPeripheral = nil } // Connect ํด์ func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) { print("โ ์ฐ๊ฒฐ ํด์ : \(peripheral.name ?? "Unknown")") connectedPeripheral = nil if let error = error { print(" Error: \(error.localizedDescription)") } } }
๐ ๏ธ 3. Peripheral Delegate
Discover services and characteristics, and read data.
BluetoothManager+Peripheral.swift โ Peripheral Delegate
import CoreBluetooth extension BluetoothManager: CBPeripheralDelegate { // ์๋น์ค ๋ฐ๊ฒฌ func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) { if let error = error { print("โ ์๋น์ค ๋ฐ๊ฒฌ ์คํจ: \(error.localizedDescription)") return } guard let services = peripheral.services else { return } print("๐ ๋ฐ๊ฒฌ๋ ์๋น์ค: \(services.count)๊ฐ") for service in services { print(" Service UUID: \(service.uuid)") // each ์๋น์ค์ ํน์ฑ ํ์ peripheral.discoverCharacteristics(nil, for: service) } } // ํน์ฑ ๋ฐ๊ฒฌ func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { if let error = error { print("โ ํน์ฑ ๋ฐ๊ฒฌ ์คํจ: \(error.localizedDescription)") return } guard let characteristics = service.characteristics else { return } print("๐ ์๋น์ค \(service.uuid)์ ํน์ฑ: \(characteristics.count)๊ฐ") for characteristic in characteristics { print(" Characteristic UUID: \(characteristic.uuid)") print(" Properties: \(characteristic.properties)") // ์ฝ๊ธฐ ๊ฐ๋ฅํ๋ฉด ์ฝ๊ธฐ if characteristic.properties.contains(.read) { peripheral.readValue(for: characteristic) } // Notify ๊ฐ๋ฅํ๋ฉด ๊ตฌ๋ if characteristic.properties.contains(.notify) { peripheral.setNotifyValue(true, for: characteristic) print(" ๐ฌ Notification ๊ตฌ๋ ") } } } // ํน์ฑ ๊ฐ ์ฝ๊ธฐ ์๋ฃ func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { if let error = error { print("โ ๊ฐ ์ฝ๊ธฐ ์คํจ: \(error.localizedDescription)") return } guard let data = characteristic.value else { print("โ ๏ธ ๋ฐ์ดํฐ ์์") return } print("๐ฅ Receive data (\(characteristic.uuid)): \(data.hexString)") // String์ผ๋ก ๋ณํ ์๋ if let stringValue = String(data: data, encoding: .utf8) { print(" String: \(stringValue)") } // ํน์ ์๋น์ค๋ณ ํ์ฑ parseCharacteristicValue(characteristic, data: data) } // ํน์ฑ ๊ฐ ์ฐ๊ธฐ ์๋ฃ func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { if let error = error { print("โ ์ฐ๊ธฐ ์คํจ: \(error.localizedDescription)") return } print("โ ์ฐ๊ธฐ ์ฑ๊ณต (\(characteristic.uuid))") } // Notification change state func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) { if let error = error { print("โ Notification ์ค์ ์คํจ: \(error.localizedDescription)") return } if characteristic.isNotifying { print("โ Notification ํ์ฑํ (\(characteristic.uuid))") } else { print("โ Notification ๋นํ์ฑํ (\(characteristic.uuid))") } } // ํน์ฑ ๊ฐ ํ์ฑ (์๋น์ค๋ณ ์ฒ๋ฆฌ) private func parseCharacteristicValue(_ characteristic: CBCharacteristic, data: Data) { // Heart Rate Service (0x180D) if characteristic.uuid == CBUUID(string: "2A37") { let heartRate = parseHeartRate(data) print(" โค๏ธ Heart Rate: \(heartRate) bpm") } // Battery Level (0x2A19) else if characteristic.uuid == CBUUID(string: "2A19") { if let batteryLevel = data.first { print(" ๐ Battery: \(batteryLevel)%") } } } // Heart Rate ๋ฐ์ดํฐ ํ์ฑ private func parseHeartRate(_ data: Data) -> Int { guard data.count > 1 else { return 0 } let flags = data[0] let is16Bit = (flags & 0x01) == 0x01 if is16Bit { return Int(data[1]) | (Int(data[2]) << 8) } else { return Int(data[1]) } } } // Data Extension extension Data { var hexString: String { map { String(format: "%02X", $0) }.joined(separator: " ") } }
๐ค 4. Writing Data
Peripheral์ ๋ฐ์ดํฐ๋ฅผ ์ ์ก.
BluetoothManager+Write.swift โ Data Writing
import CoreBluetooth extension BluetoothManager { // ๋ฌธ์์ด Send data func writeString(_ string: String, to characteristic: CBCharacteristic) { guard let data = string.data(using: .utf8) else { print("โ ๋ฌธ์์ด Conversion failed") return } writeData(data, to: characteristic) } // bytes Send data func writeData(_ data: Data, to characteristic: CBCharacteristic) { guard let peripheral = connectedPeripheral else { print("โ ์ฐ๊ฒฐ๋ ๊ธฐ๊ธฐ ์์") return } // ์ฐ๊ธฐ ๊ฐ๋ฅ ์ฌ๋ถ ํ์ธ guard characteristic.properties.contains(.write) || characteristic.properties.contains(.writeWithoutResponse) else { print("โ ์ฐ๊ธฐ ๋ถ๊ฐ๋ฅํ ํน์ฑ") return } // ์ฐ๊ธฐ ํ์ ๊ฒฐ์ let writeType: CBCharacteristicWriteType = characteristic.properties.contains(.write) ? .withResponse : .withoutResponse peripheral.writeValue(data, for: characteristic, type: writeType) print("๐ค Send data: \(data.hexString)") } // LED ์ ์ด ์์ func toggleLED(characteristic: CBCharacteristic, isOn: Bool) { let command: UInt8 = isOn ? 0x01 : 0x00 let data = Data([command]) writeData(data, to: characteristic) } }
๐ฑ 5. SwiftUI Integration
BLE ์ค์บ๋ UI implementation.
BLEScannerView.swift โ BLE Scanner UI
import SwiftUI import CoreBluetooth struct BLEScannerView: View { @State private var bluetoothManager = BluetoothManager() var body: some View { NavigationStack { VStack(spacing: 0) { // Status ํ์ statusHeader // ๊ธฐ๊ธฐ ๋ชฉ๋ก if bluetoothManager.discoveredPeripherals.isEmpty { emptyState } else { deviceList } } .navigationTitle("BLE ์ค์บ๋") .toolbar { ToolbarItem(placement: .topBarTrailing) { scanButton } } } } // Status ํค๋ private var statusHeader: some View { VStack(spacing: 8) { HStack { Circle() .fill(bluetoothManager.isBluetoothOn ? Color.green : Color.red) .frame(width: 12, height: 12) Text(bluetoothManager.isBluetoothOn ? "Bluetooth ํ์ฑ" : "Bluetooth ๋นํ์ฑ") .font(.subheadline) .foregroundStyle(.secondary) Spacer() if bluetoothManager.isScanning { ProgressView() .controlSize(.small) Text("์ค์บ ์ค...") .font(.caption) .foregroundStyle(.secondary) } } .padding() if let connected = bluetoothManager.connectedPeripheral { HStack { Image(systemName: "checkmark.circle.fill") .foregroundStyle(.green) Text("Connected: \(connected.name ?? "Unknown")") .font(.subheadline) Spacer() Button("์ฐ๊ฒฐ ํด์ ") { bluetoothManager.disconnect() } .font(.caption) .buttonStyle(.bordered) } .padding(.horizontal) .padding(.bottom) } } .background(Color(.systemGroupedBackground)) } // ์ค์บ ๋ฒํผ private var scanButton: some View { Button { if bluetoothManager.isScanning { bluetoothManager.stopScanning() } else { bluetoothManager.startScanning() } } label: { Image(systemName: bluetoothManager.isScanning ? "stop.circle" : "arrow.clockwise") } .disabled(!bluetoothManager.isBluetoothOn) } // ๋น ์ํ private var emptyState: some View { ContentUnavailableView { Label("BLE ๊ธฐ๊ธฐ๋ฅผ Not found", systemImage: "antenna.radiowaves.left.and.right.slash") } description: { Text("์ค์บ ๋ฒํผ์ ๋๋ฌ ์ฃผ๋ณ BLE ๊ธฐ๊ธฐ๋ฅผ ๊ฒ์ํ์ธ์") } } // ๊ธฐ๊ธฐ ๋ชฉ๋ก private var deviceList: some View { List(bluetoothManager.discoveredPeripherals, id: \.identifier) { peripheral in Button { bluetoothManager.connect(peripheral: peripheral) } label: { HStack { VStack(alignment: .leading, spacing: 4) { Text(peripheral.name ?? "Unknown Device") .font(.headline) Text(peripheral.identifier.uuidString) .font(.caption) .foregroundStyle(.secondary) if peripheral.state == .connected { Text("Connected") .font(.caption2) .foregroundStyle(.green) } } Spacer() Image(systemName: "chevron.right") .font(.caption) .foregroundStyle(.secondary) } } .disabled(peripheral.state == .connected) } } }
๐ก HIG Guidelines
- Permission Request: Add NSBluetoothAlwaysUsageDescription to Info.plist
- Battery Efficiency: ๋ถํ์ํ Stop scanning, ์ฐ๊ฒฐ ํ ์ฆ์ ์ค์บ ์ค๋จ
- Background: Add bluetooth-central background mode to Info.plist
- ์ฌ์ฉ์ ํผ๋๋ฐฑ: ์ค์บ ์ํ์ ์ฐ๊ฒฐ ์ํ๋ฅผ ๋ช ํํ ํ์
- Error Handling: Provide appropriate guidance for connection failures, Bluetooth off, etc.
๐ฏ Practical Usage
- Fitness Apps: ์ฌ๋ฐ์ ์ผ์, ์ค๋งํธ์์น ์ฐ๋
- IoT Control: Smart home device control
- ์๋ฃ ๊ธฐ๊ธฐ: ํ์๊ณ, ํ๋น๊ณ ๋ฐ์ดํฐ ์์ง
- ์์น ๋น์ฝ: iBeacon ๊ธฐ๋ฐ ๊ทผ์ ๋ง์ผํ
- ๊ฒ์ ์ปจํธ๋กค๋ฌ: BLE ๊ฒ์ํจ๋ ์ฐ๋
๐ Learn More
โก๏ธ Performance Tips: ์ค์บ ์
allowDuplicatesKey to false to reduce battery consumption. Filter by specific service UUIDs to find target devices faster. Always stop scanning after connecting.