Swift로 AirPods 연결 감지하기

peppermint100
PEPPERMINT100
8 min readJul 9, 2024

--

서론

지난 번에 CMHeadPhoneManager를 통해 에어팟으로 부터 자이로스코프 데이터를 받아서 얼굴 움직임을 트래킹하는 기능을 개발해 보았다. 글 마지막에 에어팟으로 줄넘기 개수 카운트 기능을 만들어보겠다고 했는데,

그 전에 먼저 에어팟 연결을 감지하는 기능을 한 번 만들어 보려고 한다.

실제로도 에어팟의 뚜껑을 열면 OS단에서 에어팟 배터리 상태를 보여주는 시트가 올라오곤 하는데, 이런 기능을 앱 단에서도 구현할 수 있을까 궁금했다.

CoreBlueTooth

에어팟은 블루투스 통신을 사용하므로 자연스럽게 블루투스 관련 모듈을 사용해보았다.

Swift에서는 CoreBlueTooth라는 API를 제공하고, 주변 블루투스 기기의 정보를 iOS를 기점 디바이스로 사용하여 스캔할 수 있었다.

가장 먼저 infoplist의 블루투스 사용 권한부터 업데이트 해주어야 한다.

그리고 CoreBluetooth관련 코드를 작성하며 이것저것 건드려보았다.

class AirPodsManager: NSObject, ObservableObject {
@Published var airPodsName: String?
@Published var airPodsModel: String?
@Published var isBluetoothAvailable: Bool = false

private var centralManager: CBCentralManager!

override init() {
super.init()
centralManager = CBCentralManager(delegate: self, queue: nil)
}
}

extension AirPodsManager: CBCentralManagerDelegate {
func centralManagerDidUpdateState(_ central: CBCentralManager) {
print("update states to = ", central.state.rawValue)
DispatchQueue.main.async {
self.isBluetoothAvailable = central.state == .poweredOn
}
self.centralManager.scanForPeripherals(withServices: nil, options: nil)
}

func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
guard let deviceName = peripheral.name else { return }
let id = peripheral.identifier
print(deviceName, " - ", id, " - ", peripheral.services)
}
}

먼저 CBCentralManager를 살펴보았다. CentralManager를 통해 주변 블루투스 기기의 상태, 정보를 업데이트 받을 수 있다.

CBCentralManager를 초기화하고 centralManagerDidUpdateState 이 메소드에서 scanForPeripherals를 실행해주면 주변 기기에 대한 정보를 받아오기 시작한다.

실제로 집에 있는 티비, 아이패드, 에어팟 잘은 모르겠지만 Dryer라는 이름의 건조기의 정보까지 받아오는 것으로 보였다.

정보를 다 허접하게 가렸지만 Device의 ID도 UUID의 형태로 가져온다.

여기서 나는 에어팟만 가져오고 싶었고, 주변 기기를 전부 스캔하는게 배터리를 많이 소모한다고 해서 코드를 수정해보았다.

먼저 scanForPeripherals의 services에 맞는 ServiceUUID를 넣으면 관련된 기기만 가져온다고 해서 ServiceUUID에 대해서 찾아보았다.

https://www.bluetooth.com/specifications/assigned-numbers/ 이러한 사이트에서 다양한 ServiceUUID를 찾고 CBUUID(string: "") 의 형태로 메소드에 넣어줬으나 어떤 값을 넣어야 오디오 장치만 찾는지는 알아내지 못했다.

func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
guard let deviceName = peripheral.name else { return }
let id = peripheral.identifier
print(deviceName, " - ", id, " - ", peripheral.services)
}

또 위 델리게이트 메소드에서 주는 정보로는 연결된 장치가 에어팟인지 알아내기가 어려웠다.

AVFAudio

결국 다른 방법을 찾아보고 있었는데, AVFAudio라는 API를 이용하면 될 것 같았다.

AVFAudio를 사용하면 NotificationCenter를 통해 OS 레벨에서 출력되는 오디오의 경로 변화를 감지해낼 수 있었다.

import Foundation
import AVFAudio

class AudioManager: NSObject, ObservableObject {
@Published var isAirPodsConnected: Bool = false
@Published var airpodsName: String = ""
private var audioSession: AVAudioSession!

override init() {
super.init()
audioSession = AVAudioSession.sharedInstance()
NotificationCenter.default.addObserver(self, selector: #selector(audioRouteChanged), name: AVAudioSession.routeChangeNotification, object: audioSession)
updateAirPodsConnectionStatus()
}

deinit {
NotificationCenter.default.removeObserver(self, name: AVAudioSession.routeChangeNotification, object: audioSession)
}

@objc private func audioRouteChanged(notification: Notification) {
updateAirPodsConnectionStatus()
}

private func updateAirPodsConnectionStatus() {
let currentRoute = audioSession.currentRoute
for output in currentRoute.outputs {
if output.portType == .bluetoothA2DP {
DispatchQueue.main.async {
self.isAirPodsConnected = true
self.airpodsName = output.portName
}
return
}
}
DispatchQueue.main.async {
self.isAirPodsConnected = false
}
}
}

위 코드는 AudioSession을 실행하고, OS단으로 부터 NotificationCenter를 통해 오디오 아웃풋의 변경을 감지한다.

이렇게 에어팟을 끼고 뺄 때마다 감지를 받아서 UI에 장치의 이름을 보여줄 수 있었다.

다만 아쉬운점이 아웃풋의 portType으로 기기를 감지하는 것인데, 에어팟은 A2DP 타입에 해당하게 된다.

A2DP란 Advance Audio Distribution Profile의 약자로 고품질 오디오를 무선으로 전송하는 BlueTooth의 타입을 의미한다.

가능하면 기기의 모델명을 받아서 Airpods인지 구분하고 싶었지만 아마 이런 기능은 지원하지 않는 것으로 보인다. (열심히 찾아봤지만..)

--

--

peppermint100
PEPPERMINT100

기억하기 위해 또는 잊어버리기 위해 작성하는 블로그입니다.