AVFoundation
AVFoundation은 사진, 동영상, 오디오 등을 재생/캡쳐/처리 하는데 있어 만능 도구라 볼 수 있다.
그 중 카메라로 캡쳐하는 기능을 만들거라 AVFoundation Capture subsystem을 사용해보겠다.
Capture Architecture

캡쳐 아키텍처에 대한 이해가 있으면 좋기 때문에 간단히 설명을 해보자면,
캡쳐 아키텍처에서 중요한 부분은 Session, Input, Output이다.
AVCaptureSession : 하나 이상의 Input을 하나 이상의 output과 연결한다.
AVCaptureDeviceInput : iOS 기기나 Mac에 내장된 카메라/마이크와 같은 캡처 장치를 포함한 미디어 소스다.
AVCaptureOutput : Input에서 미디어를 수집하여 디스크에 기록된 동영상 파일이나 라이브 처리에 사용할 수 있는 원시 픽셀 버퍼와 같은 유용한 데이터를 생성한다.
비유하자면, Session은 빨대이며 Input과 Output은 각각의 구멍이다.
카메라 만들기
우선 폴더 형태는 CameraFeature + ContentView로 이루어져 있다.

Info.plist 설정
TARGETS -> Info 에 들어가서 Privacy - Camera Usage Description 을 추가해주고 카메라 권한 허용하라는 내용을 적어준다.

ContentView
import SwiftUI
struct ContentView: View {
@State var isPresented: Bool = false
var body: some View {
VStack {
Button("카메라 열기") {
isPresented = true
}
}
.sheet(isPresented: $isPresented) {
CameraView()
}
}
}
우선 ContentView에서는 sheet를 통해 카메라 뷰를 띄울거라 아주 간단하게 작성해봤다.
CameraView
import SwiftUI
struct CameraView: View {
@StateObject private var viewModel: CameraViewModel = CameraViewModel()
@Environment(\.dismiss) private var dismiss
var body: some View {
ZStack {
//A : 캡쳐된 이미지 (사진 찍혔을때)
if let image = viewModel.capturedImage {
Image(uiImage: image)
.resizable()
.scaledToFit()
.frame(maxWidth: .infinity, maxHeight: .infinity)
//다시 찍기 버튼
.overlay(alignment: .bottom) {
Button("다시 찍기") {
viewModel.retake()
}
}
}
//B : 카메라 프리뷰 (카메라 렌즈로 보이는 뷰)
else {
CameraPreviewView(session: viewModel.captureSession)
//카메라 전환 버튼
.overlay(alignment: .bottomLeading) {
Button("카메라 전환") {
viewModel.switchCamera()
}
}
//사진 촬영 버튼
.overlay(alignment: .bottom) {
Button("사진 촬영") {
viewModel.takePhoto()
}
}
}
}
//카메라 권한 없을때 알럿
.alert("카메라 권한이 필요해요", isPresented: $viewModel.isAlertPresented) {
Button("취소") { dismiss() }
Button("설정으로 이동") {
viewModel.goSetting()
dismiss()
}
}
.onAppear{
Task { await viewModel.checkCameraAuth() }
}
}
}
카메라가 띄워질 뷰다. 카메라로 찍힌 사진을 보여줄 A뷰와 카메라 렌즈로 실제 세상을 보여줄 B뷰로 나누었다.
그리고 사진을 찍었어도 다시 찍기 위한 버튼을 A뷰에 뒀고, 카메라 전환과 사진 촬영을 위한 버튼을 B뷰에 뒀다.
또한 카메라 권한이 없다면 카메라가 안켜지기에 그 부분을 처리할 alert을 뒀다.
마지막으로 카메라 뷰가 나타날때 권한 체크를 위한 함수까지 뒀다.
CameraViewModel
import SwiftUI
import AVFoundation
class CameraViewModel: NSObject, ObservableObject {
@Published var capturedImage: UIImage? //캡쳐된 이미지
@Published var isAlertPresented: Bool = false //카메라 권한 없을때 띄울 alert
let captureSession = AVCaptureSession() //capture세션
private let photoOutput = AVCapturePhotoOutput() //cature output
private var currentPosition: AVCaptureDevice.Position = .back //카메라 전면 or 후면 위치
private let discoverySession = AVCaptureDevice.DiscoverySession( // DiscoverySession: 기기 목록 필터링
deviceTypes: [.builtInTrueDepthCamera, .builtInDualCamera, .builtInWideAngleCamera],
mediaType: .video,
position: .unspecified
)
var isAuthorized: Bool { //카메라 권한 있는지 계산 프로퍼티의 (get은 읽어들일때마다 실행)
get async {
let status = AVCaptureDevice.authorizationStatus(for: .video)
var isAuthorized = status == .authorized
if status == .notDetermined {
isAuthorized = await AVCaptureDevice.requestAccess(for: .video) //카메라 권한 요청
}
return isAuthorized
}
}
//MARK: - 카메라 권한 확인
func checkCameraAuth() async {
//A : 카메라 권한이 없을시 alert 띄우기
guard await isAuthorized else {
await MainActor.run {
isAlertPresented = true
}
return
}
//B : 카메라 권한이 있을시 캡쳐 세션 세팅
setUpCaptureSession()
}
//MARK: - 권한 설정으로 이동
func goSetting() {
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
}
//MARK: - 사진 촬영
func takePhoto() {
let settings = AVCapturePhotoSettings()
settings.flashMode = .auto
photoOutput.capturePhoto(with: settings, delegate: self)
}
//MARK: - 다시 찍기
func retake() {
capturedImage = nil
Task {
captureSession.startRunning()
}
}
//MARK: - 카메라 전환
func switchCamera() {
// <---- 설정변경 시작 ---->
captureSession.beginConfiguration()
if let currentInput = captureSession.inputs.first as? AVCaptureDeviceInput {
captureSession.removeInput(currentInput) // 현재 Input 제거
currentPosition = (currentInput.device.position == .back) ? .front : .back // 반대쪽 카메라 위치 선택
if let newInput = try? AVCaptureDeviceInput(device: bestDevice(in: currentPosition)), // 새로운 Input 설정
captureSession.canAddInput(newInput) {
captureSession.addInput(newInput)
}
}
captureSession.commitConfiguration()
// <---- 설정변경 완료 ---->
}
//MARK: - 캡쳐 세션 세팅
private func setUpCaptureSession() {
// <---- 설정변경 시작 ---->
captureSession.beginConfiguration()
guard let videoDeviceInput = try? AVCaptureDeviceInput(device: bestDevice(in: currentPosition)),
captureSession.canAddInput(videoDeviceInput)
else { return }
captureSession.addInput(videoDeviceInput)
guard captureSession.canAddOutput(photoOutput) else { return }
captureSession.sessionPreset = .photo
captureSession.addOutput(photoOutput)
captureSession.commitConfiguration()
// <---- 설정변경 완료 ---->
captureSession.startRunning()
}
// MARK: - 최적 카메라 선택
private func bestDevice(in position: AVCaptureDevice.Position) -> AVCaptureDevice {
let devices = self.discoverySession.devices
guard !devices.isEmpty else { fatalError("Missing capture devices.")}
return devices.first(where: { device in device.position == position })!
}
}
// MARK: - AVCapturePhotoCaptureDelegate(사진 촬영 후 이미지화를 위한)
extension CameraViewModel: AVCapturePhotoCaptureDelegate {
func photoOutput(_ output: AVCapturePhotoOutput,
didFinishProcessingPhoto photo: AVCapturePhoto,
error: Error?) {
guard let imageData = photo.fileDataRepresentation(),
let image = UIImage(data: imageData) else { return }
DispatchQueue.main.async {
self.capturedImage = image
}
captureSession.stopRunning()
}
}
코드가 굉장히 많긴 하지만 차근차근 보자.
일단 CameraView에서 checkCameraAuth() 함수가 호출 될 때부터 시작된다.
//MARK: - 카메라 권한 확인
func checkCameraAuth() async {
//A : 카메라 권한이 없을시 alert 띄우기
guard await isAuthorized else {
await MainActor.run {
isAlertPresented = true
}
return
}
//B : 카메라 권한이 있을시 캡쳐 세션 세팅
setUpCaptureSession()
}
카메라 권한이 없다면 isAlertPresented를 true로 만들어 alert을 띄워준다.
카메라 권한이 있다면 캡쳐 세션을 세팅해주는 setUpCaptureSession() 메서드를 호출한다.
isAuthorized 는 계산프로퍼티로 카메라 권한 상태를 관리하며 권한이 정해지지 않았으면 사용자에게 권한요청을 보낸다
다음으로 setUpCaptureSession() 메서드를 살펴보겠다.
//MARK: - 캡쳐 세션 세팅
private func setUpCaptureSession() {
// <---- 설정변경 시작 ---->
captureSession.beginConfiguration()
guard let videoDeviceInput = try? AVCaptureDeviceInput(device: bestDevice(in: currentPosition)),
captureSession.canAddInput(videoDeviceInput)
else { return }
captureSession.addInput(videoDeviceInput)
guard captureSession.canAddOutput(photoOutput) else { return }
captureSession.sessionPreset = .photo
captureSession.addOutput(photoOutput)
captureSession.commitConfiguration()
// <---- 설정변경 완료 ---->
captureSession.startRunning()
}
이 메서드는 앞서 말했던 Session을 설정하는 기능을 한다.
beginConfiguration()과 commitConfiguration()은 각각 세션의 설정변경 시작을 알리고 변경된 설정을 저장하는 역할을 해주며, 세션 설정을 변경해줄때는 꼭 이 두 가지 메서드 사이에서 해줘야한다!
먼저 세션에 Input을 붙이기 위해서는 카메라 장치(AVCaptureDevice)를 가져와야하는데 나는 bestDevice(in: )라는 메서드를 구현해 사용했다.
그후, 세션에 Input을 붙일 수 있는지 확인하고 붙이는 작업을 했다.
그리고 세션에 Output을 붙이기 위해서는 사진을 찍어 내보내는 장치(AVCapturePhotoOutput의 인스턴스)가 필요해 넣어주었고, sessionPreset = .photo를 통해 사진 촬영에 적합한 화질을 설정해주었다. 그후, 세션에 Output을 붙이는 작업을 해주었다.
(빨대에 구멍이 하나만 있으면 안되니 Input, Output 모두 뚫어줘야 한다)
여기까지만 해줘도 카메라를 시동시킬 수 있는 것이다 !!
다음으로는 카메라 전환 기능(전면/후면) switchCamera() 메서드를 보겠다.
//MARK: - 카메라 전환
func switchCamera() {
// <---- 설정변경 시작 ---->
captureSession.beginConfiguration()
if let currentInput = captureSession.inputs.first as? AVCaptureDeviceInput {
captureSession.removeInput(currentInput) // 현재 Input 제거
currentPosition = (currentInput.device.position == .back) ? .front : .back // 반대쪽 카메라 위치 선택
if let newInput = try? AVCaptureDeviceInput(device: bestDevice(in: currentPosition)), // 새로운 Input 설정
captureSession.canAddInput(newInput) {
captureSession.addInput(newInput)
}
}
captureSession.commitConfiguration()
// <---- 설정변경 완료 ---->
}
카메라를 전환한다는 것은, 세션 설정을 다시 해줘야 한다는 것이다.
그래서 처음 세션 설정을 해준것처럼 beginConfiguration() 과 commitConfiguration() 사이에서 세션 변경을 해준다.
이렇게 간단하게 카메라 전환기능도 만들 수 있다 !
자, 여기서 두 번이나 쓰인 bestDevice(in: ) 메서드와 discoverySession 을 살펴보자.
// DiscoverySession: 기기 목록 필터링
private let discoverySession = AVCaptureDevice.DiscoverySession(
deviceTypes: [.builtInTrueDepthCamera, .builtInDualCamera, .builtInWideAngleCamera],
mediaType: .video,
position: .unspecified
)
// MARK: - 최적 카메라 선택
private func bestDevice(in position: AVCaptureDevice.Position) -> AVCaptureDevice {
let devices = self.discoverySession.devices
guard !devices.isEmpty else { fatalError("Missing capture devices.")}
return devices.first(where: { device in device.position == position })!
}
먼저 AVCaptureDevice.DiscoverySession은 카메라 장치 목록을 가져오는 필터 같은 역할을 한다.
bestDevice(in: ) 메서드는 카메라 위치(전면/후면)을 인자로 받는다.
아까처럼 세션 Input에 카메라를 연결해주는 역할을 하는 것이다 !
다음으로는 사진 촬영 기능에 대해 볼 것이므로 takePhoto()와 AVCapturePhotoCaptureDelegate에 대해 보겠다.
//MARK: - 사진 촬영
func takePhoto() {
let settings = AVCapturePhotoSettings()
settings.flashMode = .auto
photoOutput.capturePhoto(with: settings, delegate: self)
}
// MARK: - AVCapturePhotoCaptureDelegate(사진 촬영 후 이미지화를 위한)
extension CameraViewModel: AVCapturePhotoCaptureDelegate {
func photoOutput(_ output: AVCapturePhotoOutput,
didFinishProcessingPhoto photo: AVCapturePhoto,
error: Error?) {
guard let imageData = photo.fileDataRepresentation(),
let image = UIImage(data: imageData) else { return }
DispatchQueue.main.async {
self.capturedImage = image
}
captureSession.stopRunning()
}
}
takePhoto() 메서드에서
photoOutput(_:didFinishProcessingPhoto:...) 딜리게이트가 호출되면
이렇게 delegate패턴을 통해 아주 쉽게 사진 촬영 기능도 만들 수 있다.
만약 사진 촬영 후 다시 찍고 싶다면,
//MARK: - 다시 찍기
func retake() {
capturedImage = nil
Task {
captureSession.startRunning()
}
}
capturedImage를 nil로 만들어주고 세션을 startRunning()로 다시 실행시켜주면 된다 !
자 이제 카메라 기능들의 이야기가 끝났으니, 진짜로 카메라 프리뷰와 연결시켜주는 작업을 해줘야한다 !
CameraPreviewView
카메라 프리뷰 뷰는 카메라 렌즈를 통해 보이는 실제세상의 뷰다.
import SwiftUI
import AVFoundation
struct CameraPreviewView: UIViewRepresentable {
let session: AVCaptureSession
class PreviewView: UIView {
override class var layerClass: AnyClass {
return AVCaptureVideoPreviewLayer.self
}
var videoPreviewLayer: AVCaptureVideoPreviewLayer {
return layer as! AVCaptureVideoPreviewLayer
}
}
func makeUIView(context: Context) -> PreviewView {
let view = PreviewView()
view.videoPreviewLayer.session = session //세션 일치 시켜주기
return view
}
func updateUIView(_ uiView: PreviewView, context: Context) {
//업뎃 로직 필요시
}
}
SwiftUI 자체에는 카메라 미리보기 기능이 없으니까, UIKit의 AVCaptureVideoPreviewLayer를 감싸서 써야한다.
여기서 주의할 점은 videoPreviewLayer의 세션에 우리가 만들어 둔 세션을 연결시켜줘야 카메라가 정상적으로 작동하고 전환/촬영 기능도 작동한다.
여기까지 했다면 나만의 카메라 만들기에 성공했을 것이다.
구현영상
