CMHeadPhoneManager 사용해보기

peppermint100
PEPPERMINT100
Published in
9 min readJun 23, 2024

서론

CMHeadPhoneManager는 사용자의 에어팟 혹은 에어팟 맥스에 내장된 모션 감지기를 통해 움직임 관련된 데이터를 받을 수 있도록 만들어진 API이다.

이러한 기능을 이용한 앱 중에는 PosturePod이라는 앱이 있다. 에어팟을 착용하고 올바른 자세와 거북목 자세를 저장해두고 인식해서 사용자가 잘못된 자세로 있으면 알려주는 앱이다.

아이디어가 신기하다고 평소에 생각했는데, 그 기반에는 CMHeadPhoneManager가 있을 것으로 생각이 들어서 간단히 사용해보기로 했다.

나는 여기서 에어팟을 낀채로 머리를 움직이면 그 머리 움직임을 추적하는 간단한 기능을 구현해보기로 했다.

앞으로 이와 같이유용하거나 재밌어 보이는 Native 기능들을 한 번씩 사용해보려고 한다.

권한 확인

러닝 앱을 만들 때도 사용해보았지만 CoreMotion, CoreLocation과 같은 API는 Info.plist를 통해 권한을 받는다.

Info.plist의 Motion Usage Description을 추가해준다.

private func updateAuthorization() {
switch CMHeadphoneMotionManager.authorizationStatus() {
case .authorized:
isHeadPhoneAuthorized = true
case .denied:
break
case .notDetermined:
break
case .restricted:
break
@unknown default:
break
}
}

그 후 허용되면 로컬 변수인 isHeadPhoneAuthorized 를 변경해준다.

Text(
headPhoneManager.isHeadPhoneAuthorized ? "헤드폰 권한허용! 🎧" : "헤드폰 권한 미허용 😇"
)

그 다음 View에 위와 같이 허용을 받았는지 안받았는지 나타내주는 텍스트를 추가해주었다.

Pitch, Roll, Yaw

이제 CMHeadPhoneManager에서 받을 수 있는 값들에 대해서 살펴보았다.

다른 CoreMotion 프레임워크와 똑같이 .startDeviceMotionUpdates 메소드로 부터 다양한 값을 지속적으로 받을 수 있는데,

이 문서를 확인하면 deviceMotion이라는 값을 받을 수 있다고 한다.

deviceMotion안에 CMAttitute라는 값이 있다. 그리고 그 CMAttitue안에 roll, pitch, yaw라는 값이 있다.

항공학부를 다닐 시절에 유체역학쪽에서 잠시 볼 수 있었던 값이다.

GPT에게 물어보면 이런 답변을 준다. 여튼 Roll은 x축, Pitch는 y축, Yaw는 z축에 해당한다는 사실만 알면 될 것 같다.

이제 이 값들을 받아서 업데이트를 하면 될 것 같다.

얼굴 움직임 트래킹

얼굴의 움직임을 그대로 트래킹하기 위해 타원형 모양의 Capsule을 하나 만들고 에어팟의 roll, pitch, yaw를 받아서 움직여보도록 해보았다.

먼저 CoreMotion으로 부터 값을 받는다.

    
func startUpdates() {
print("헤드폰 모션 추적을 시작합니다.")
if isTracking {
print("이미 헤드폰 모션을 추적중입니다.")
return
}

isTracking = true

cmHeadPhoneManager.startDeviceMotionUpdates(to: .main) { [weak self] motion, error in
guard let motion = motion, let self = self, error == nil else {
print("HeadPhone CoreMotion에서 에러가 발생했습니다. error = ", error!.localizedDescription)
return
}

DispatchQueue.main.async {
self.pitch = motion.attitude.pitch
self.roll = motion.attitude.roll
self.yaw = motion.attitude.yaw
}
}
}

이렇게 pitch, roll, yaw안에 attitute 내부에서 주는 값들을 할당해주었다. startDeviceMotionUpdates는 Operation Queue에서 동작하고 pitch, roll, yaw의 값은 뷰에서 변화를 주므로 메인 스레드로 돌려주었다.

그리고 뷰에서

Capsule()
.fill(Color.blue)
.frame(width: 150, height: 200)
.rotation3DEffect(
.radians(headPhoneManager.pitch),
axis: (x: 1, y: 0, z: 0)
)
.rotation3DEffect(
.radians(headPhoneManager.yaw),
axis: (x: 0, y: 1, z: 0)
)

.rotation3DEffect(
.radians(headPhoneManager.roll),
axis: (x: 0, y: 0, z: 1)
)

이렇게 변하는 값들로 Capsule에 rotation3DEffect를 주면 된다. rotation3DEffect는 degree나 radians로 각도를 받는데, CMHeadPhoneManager의 attitute는 라디안으로 값을 바로 주므로 그대로 할당시켜준다.

그런데 GPT에 따르면 pitch는 y, roll이 x, yaw는 z축이라고 했는데 rotation3DEffect의 axis는 다른 값들을 각각 할당해주고 있다. 이부분은 핸드폰과 항공기의 차이인데,

이 때 아래 비행기를 기준으로 pitch, yaw, roll의 회전축을 보자

이 그림대로 항공기의 머리쪽이 스크린쪽 꼬리쪽이 핸드폰의 뒤쪽 카메라쪽이라고 생각해보자.

그러면 보통 핸드폰의 가로는 x, 세로는 y로 보통 프레임이나 오프셋을 계산하므로 핸드폰의 z는 Roll axis가 된다. 따라서 rotation3DEffect에서 roll axis에 z를 준다.

이제 앱을 실행하고 확인해보니

음 의도하던대로 움직이기는 했으나, 사실 사람들마다 에어팟의 착용 방식도 다르고 고개를 어디로 해두고 측정을 시작하냐에 따라서도 다른 값을 보여주었다.

개선

먼저 시작버튼을 누를 때 최초의 움직임을 저장하도록 했다.

func calibrate() {
initialPitch = pitch
initialRoll = roll
initialYaw = yaw
}

그리고 CoreMotion에서 값이 변화할 때마다 최초 계산된 최초 위치를 뺀 값으로 하여 최초 위치에서 움직인 정도를 Capsule에 반영해도록 해주었다.

 cmHeadPhoneManager.startDeviceMotionUpdates(to: .main) { [weak self] motion, error in
guard let motion = motion, let self = self, error == nil else {
print("HeadPhone CoreMotion에서 에러가 발생했습니다. error = ", error!.localizedDescription)
return
}

DispatchQueue.main.async {
self.pitch = motion.attitude.pitch - self.initialPitch
self.roll = motion.attitude.roll - self.initialRoll
self.yaw = motion.attitude.yaw - self.initialYaw
}
}

추가로 움직임이 좀 더 잘보이게 하기 위하여 Capsule에 그라데이션 색상을 입혀주었다.

Capsule()
.fill(
LinearGradient(
gradient: Gradient(colors: [Color.red, Color.blue]),
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
.frame(width: 150, height: 200)
.rotation3DEffect(
.radians(headPhoneManager.pitch),
axis: (x: 1, y: 0, z: 0)
)
.rotation3DEffect(
.radians(headPhoneManager.yaw),
axis: (x: 0, y: 1, z: 0)
)

.rotation3DEffect(
.radians(headPhoneManager.roll),
axis: (x: 0, y: 0, z: 1)
)

LinearGradient를 통해 색상을 그라데이션으로 넣어줌으로서 변화할 때 움직임이 좀 더 동적으로 잘 확인되도록 했다.

이제 좀 더 자연스럽고 움직임이 잘 보이는 Capsule이 완성되었다. 탭바를 보면 Jump..라는 탭이 있는데, 가능하다면 중력 벡터를 이용해서 점프 횟수를 세서 줄넘기 할 때 머릿속으로 세지 않고도 몇 번 뛰었는지 세주는 기능을 만들어 보려고 한다!

전체 코드는 아래에서 확인할 수 있다.

https://github.com/peppermint100/ios-technologies/tree/master/AirPodsCoreMotion

--

--

peppermint100
PEPPERMINT100

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