๐ŸŒ KO

๐Ÿ“ถ Core Bluetooth

โญ Difficulty: โญโญโญโญ โฑ๏ธ Est. Time: 3-4h ๐Ÿ“‚ System & Network

BLE ๊ธฐ๊ธฐ ์Šค์บ”, ์—ฐ๊ฒฐ, ๋ฐ์ดํ„ฐ ํ†ต์‹  ํ”„๋ ˆ์ž„์›Œํฌ

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 ์ดˆ๊ธฐํ™”

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)
    }

    // ์Šค์บ” ์‹œ์ž‘
    func startScanning() {
        guard isBluetoothOn else {
            print("โš ๏ธ Bluetooth๊ฐ€ ๊บผ์ ธ ์žˆ์Šต๋‹ˆ๋‹ค")
            return
        }

        discoveredPeripherals.removeAll()
        isScanning = true

        // ๋ชจ๋“  BLE ๊ธฐ๊ธฐ ์Šค์บ” (์„œ๋น„์Šค ํ•„ํ„ฐ ์—†์Œ)
        centralManager.scanForPeripherals(
            withServices: nil,
            options: [CBCentralManagerScanOptionAllowDuplicatesKey: false]
        )

        print("๐Ÿ” BLE ๊ธฐ๊ธฐ ์Šค์บ” ์‹œ์ž‘")
    }

    // ํŠน์ • ์„œ๋น„์Šค๋งŒ ์Šค์บ”
    func scanForDevices(withServices serviceUUIDs: [CBUUID]) {
        guard isBluetoothOn else { return }

        discoveredPeripherals.removeAll()
        isScanning = true

        centralManager.scanForPeripherals(withServices: serviceUUIDs, options: nil)
        print("๐Ÿ” ํŠน์ • ์„œ๋น„์Šค ์Šค์บ”: \(serviceUUIDs)")
    }

    // ์Šค์บ” ์ค‘์ง€
    func stopScanning() {
        centralManager.stopScan()
        isScanning = false
        print("โน๏ธ ์Šค์บ” ์ค‘์ง€")
    }

    // Peripheral ์—ฐ๊ฒฐ
    func connect(peripheral: CBPeripheral) {
        stopScanning()
        centralManager.connect(peripheral, options: nil)
        print("๐Ÿ”— ์—ฐ๊ฒฐ ์‹œ๋„: \(peripheral.name ?? "Unknown")")
    }

    // ์—ฐ๊ฒฐ ํ•ด์ œ
    func disconnect() {
        guard let peripheral = connectedPeripheral else { return }
        centralManager.cancelPeripheralConnection(peripheral)
        print("โŒ ์—ฐ๊ฒฐ ํ•ด์ œ")
    }
}

๐Ÿ“ก 2. Central Manager ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ

Handle Bluetooth state changes and device discovery.

BluetoothManager+Delegate.swift โ€” Central Delegate
import CoreBluetooth

extension BluetoothManager: CBCentralManagerDelegate {
    // Bluetooth ์ƒํƒœ ๋ณ€ํ™”
    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 ๋ฏธ์ง€์›")
            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)")
        }
    }

    // ์—ฐ๊ฒฐ ์„ฑ๊ณต
    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        print("โœ… ์—ฐ๊ฒฐ ์„ฑ๊ณต: \(peripheral.name ?? "Unknown")")
        connectedPeripheral = peripheral
        peripheral.delegate = self

        // ์„œ๋น„์Šค ํƒ์ƒ‰
        peripheral.discoverServices(nil)
    }

    // ์—ฐ๊ฒฐ ์‹คํŒจ
    func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
        print("โŒ ์—ฐ๊ฒฐ ์‹คํŒจ: \(error?.localizedDescription ?? "Unknown error")")
        connectedPeripheral = nil
    }

    // ์—ฐ๊ฒฐ ํ•ด์ œ
    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 ๋ธ๋ฆฌ๊ฒŒ์ดํŠธ

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)")
            // ๊ฐ ์„œ๋น„์Šค์˜ ํŠน์„ฑ ํƒ์ƒ‰
            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("๐Ÿ“ฅ ๋ฐ์ดํ„ฐ ์ˆ˜์‹  (\(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 ์ƒํƒœ ๋ณ€๊ฒฝ
    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. ๋ฐ์ดํ„ฐ ์“ฐ๊ธฐ

Peripheral์— ๋ฐ์ดํ„ฐ๋ฅผ ์ „์†ก.

BluetoothManager+Write.swift โ€” Data Writing
import CoreBluetooth

extension BluetoothManager {
    // ๋ฌธ์ž์—ด ๋ฐ์ดํ„ฐ ์ „์†ก
    func writeString(_ string: String, to characteristic: CBCharacteristic) {
        guard let data = string.data(using: .utf8) else {
            print("โŒ ๋ฌธ์ž์—ด ๋ณ€ํ™˜ ์‹คํŒจ")
            return
        }

        writeData(data, to: characteristic)
    }

    // ๋ฐ”์ดํŠธ ๋ฐ์ดํ„ฐ ์ „์†ก
    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("๐Ÿ“ค ๋ฐ์ดํ„ฐ ์ „์†ก: \(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) {
                // ์ƒํƒœ ํ‘œ์‹œ
                statusHeader

                // ๊ธฐ๊ธฐ ๋ชฉ๋ก
                if bluetoothManager.discoveredPeripherals.isEmpty {
                    emptyState
                } else {
                    deviceList
                }
            }
            .navigationTitle("BLE ์Šค์บ๋„ˆ")
            .toolbar {
                ToolbarItem(placement: .topBarTrailing) {
                    scanButton
                }
            }
        }
    }

    // ์ƒํƒœ ํ—ค๋”
    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.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 ๊ธฐ๊ธฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค", 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("์—ฐ๊ฒฐ๋จ")
                                .font(.caption2)
                                .foregroundStyle(.green)
                        }
                    }

                    Spacer()

                    Image(systemName: "chevron.right")
                        .font(.caption)
                        .foregroundStyle(.secondary)
                }
            }
            .disabled(peripheral.state == .connected)
        }
    }
}

๐Ÿ’ก HIG Guidelines

๐ŸŽฏ Practical Usage

๐Ÿ“š 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.

๐Ÿ“Ž Apple Official Resources

๐Ÿ“˜ Documentation ๐Ÿ’ป Sample Code ๐ŸŽฌ WWDC Sessions