iOS 카메라 개발기 - 프리뷰가 버벅일 때 먼저 볼 것들
MTKView가 카메라와 다른 박자로 돌고 있었고, 페이지마다 Metal 렌더러를 만들고 있었다. 화면이 끊길 때 의심할 순서를 적었다.
egg2분 읽기
카메라 프리뷰를 AVCaptureVideoPreviewLayer에 맡기지 않고 Metal로 직접 그리고 있다. 셰이더로 원하는 걸 다 할 수 있는 대신 화면이 끊기는 이유도 전부 내 몫이 됐다.
지난 편까지는 버퍼와 파일 이야기였고 이번 편부터는 화면이다. 기기는 iPhone 17 Pro, iOS 26.6.1.
MTKView가 자기 박자로 돌고 있었다
카메라는 30fps로 프레임을 주는데 MTKView는 기본 60fps로 그린다. 같은 프레임을 두 번씩 그리고 있었다.
그래서 MTKView를 30fps로 맞췄다. 그래도 끊겼다. 박자는 같아도 위상이 어긋나서 “같은 프레임을 두 번 그림”과 “새 프레임이 다음 것에 덮여 한 번도 안 그려짐”이 번갈아 났다.
결국 MTKView의 디스플레이 링크를 끄고 카메라 프레임이 올 때만 그리게 했다.
mtkView.isPaused = true
mtkView.enableSetNeedsDisplay = true
// 캡처 큐에서 새 프레임을 받으면
DispatchQueue.main.async { mtkView.setNeedsDisplay() }
두 플래그는 같이 걸어야 한다. 하나만 걸면 화면이 멈추거나 링크가 그대로 돈다.
크기가 바뀌었을 때처럼 프레임과 무관하게 다시 그려야 하는 경우는 drawableSizeWillChange에서 직접 한 번 그린다.
셔터를 누를 때마다 프리뷰가 얼었다
촬영할 때마다 프리뷰가 잠깐 멈췄다. 따라가 보니 12MP 버퍼의 getBytes와 인코딩이 메인 스레드에서 돌고 있었다. 중간에 48MB짜리 복사가 두 번 있었다.
업로드는 CVMetalTextureCache로 복사 없이 텍스처를 만들고 풀해상도 렌더와 인코딩은 전용 큐로 뺐다.
페이지마다 렌더러를 만들고 있었다
좌우로 넘기는 TabView 화면에 들어가는 데 1초가 걸렸다. TabView는 ForEach 안의 페이지 모델을 보이는 것만 만드는 게 아니라 전부 만든다. 페이지마다 Metal 렌더러를 하나씩 만들고 있었으니 페이지 수만큼 렌더러가 생긴 셈이다.
한 벌을 만들어 페이지들이 공유하게 하니 바로 열렸다.
갤러리 목록도 비슷했다. 셀의 .task에서 12MP HEIC를 디코드했는데 .task는 MainActor에서 돈다. 스크롤이 멈추는 게 당연했다. 디코드는 Task.detached로 빼고 결과는 NSCache에 담았다.
캐시는 그리는 자리에서 동기로 읽어야 한다. .task에서 비동기로 꺼내면 셀이 다시 만들어질 때마다 회색이 한 프레임 번쩍인다.
레이아웃 위치를 PreferenceKey로 올려서 @State에 받았더니 LazyVStack의 셀 생성과 되먹임 루프가 생겼다. 위치가 바뀌면 셀이 생기고 셀이 생기면 위치가 바뀐다. 다시 그릴 필요가 없는 값이라 참조 객체에 쓰니 루프가 끊겼다.
카메라 전환 때 보이던 검은 화면
전후면을 바꾸면 세션을 재구성하는 동안 검은 화면이 보였다.
그래서 덮었다. 전환을 시작할 때 UIVisualEffectView를 올리고 새 카메라의 첫 프레임이 도착하면 걷는다. 블러는 뒤 레이어를 실시간으로 흐리니까 마지막 프레임을 스냅샷으로 떠둘 필요가 없다.
줌 전환이 남긴 줄무늬
iOS 18의 줌 내비게이션 전환을 썼다. 목적지 뷰가 통째로 스케일되는 방식이고 진행률을 알려주는 API는 없다. 쓸 수 있는 건 ZoomNavigationTransition 하나다.
스케일되는 동안 목록에 가로줄 잔상이 흘렀다. pinnedViews: .sectionHeaders로 고정한 헤더가 매 프레임 자리를 다시 잡고 있었다. 단서는 “목록 맨 위에서는 안 난다”였다. 헤더가 고정될 일이 없는 위치에서만 멀쩡했으니까.
.regularMaterial 바도 비슷한 줄무늬를 냈다. 스케일되는 동안 아래를 계속 다시 흐리기 때문이다.
하나 더. 나중에 navigationTransition을 바꿔도 이미 붙은 끌어 닫기 제스처는 떨어지지 않는다. 그 제스처는 interactivePopGestureRecognizer와 다른 인식기라서 그쪽을 꺼도 소용없다.
Liquid Glass를 글자 위에 얹었더니
유리 효과를 글자 위에 얹으면 굴절 대상이 글자 자신이 되어 여러 겹으로 번진다. 글자 뒤 배경에만 깐다.
카메라 화면은 배경이 대부분 검정이라 반투명 검정이 안 보인다. glassEffect(.clear)(iOS 26)를 쓰고 이전 OS에서는 흰색으로 대신했다.
시스템 .buttonStyle(.glass)는 캡슐이 버튼 프레임에 붙어 있다. 캡슐은 고정해두고 그 아래로 항목이 지나가는 형태에는 못 쓴다.
정리하고 보니 버벅임은 대부분 모르는 사이에 같은 일을 여러 번 하던 데서 나온 것 같다. 다음부터는 화면이 끊기면 뭘 두 번 하고 있는지부터 봐야겠다.
다음 편은 같은 화면의 제스처다. 손을 떼기도 전에 onEnded가 불리던 이야기.
Filed under 개발