SwiftUI에서 카메라 사용하기

shintwl·2024년 4월 11일
0

SwiftUI에서 카메라(AVFoundation)을 사용하려면 어떻게 해야할지 알아봅시다
실제 기기가 필요합니다 (시뮬레이터는 안돼요)

요약

AVFoundation으로 카메라를 설정&사용하는 View를 생성합니다
UIViewRepresentable를 통해 SwiftUI에서 불러옵니다

기본 화면 만들기 (SwiftUI)

간단한 기본화면을 만들어보겠습니다
버튼 하나를 둬서 전면/후면이 전환되도록 하겠습니다

struct ContentView: View {
    var body: some View {
        VStack {
            Spacer()
            Spacer()
            HStack {
                Spacer()
                Spacer()
                Button(action: {print("Hello")}) {
                    Image(systemName: "arrow.triangle.2.circlepath.camera")
                        .resizable()
                        .scaledToFit()
                        .frame(width: 50, height: 50)
                }.padding(.trailing, 20)
            }
        }
    }
}

카메라 화면 만들기 (UIViewRepresentable)

PreviewView

레이어를 AVCaptureVideoPreviewLayer로 가지고 있는 UIView를 만들어 줍니다
AVCaptureSession에서 나온 AVCaptureVideoDataOutput이 바로 이 레이어로 연결됩니다

class PreviewView:UIView {
    override class var layerClass: AnyClass {
        return AVCaptureVideoPreviewLayer.self
    }
    
    var previewLayer: AVCaptureVideoPreviewLayer? {
        return layer as? AVCaptureVideoPreviewLayer
    }
}

CameraPreviewView

PreviewView를 SwiftUI로 바꿔줍니다
PreviewView를 생성하면서 카메라 설정도 해줍니다

queue 생성

카메라 제어할 serial queue를 생성합니다

private let cameraQueue = DispatchQueue(label: "cameraQueue")

AVCaptureSession 설정

카메라의 input과 output을 관리하는 객체입니다

private var captureSession = AVCaptureSession()

AVCaptureVideoDataOutput 설정

카메라로부터 받아온 데이터를 사용할 곳을 지정합니다

let videoOutput = AVCaptureVideoDataOutput()
self.captureSession.addOutput(videoOutput)

AVCaptureDevice 설정

어떤 카메라 하드웨어를 사용할 지 설정합니다
AVCaptureDevice.DiscoverySession를 통해 질의하여 조건에 맞는 하드웨어를 탐색합니다
앞면, 뒷면 카메라를 모두 사용하기 때문에 카메라 위치는 변수로 받아줍니다

mutating func configureCamera(cameraPosition: AVCaptureDevice.Position) {
    let discoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera,.builtInUltraWideCamera], mediaType: .video, position: cameraPosition)
        
    guard let cameraDevice = discoverySession.devices.first else {
       fatalError("no camera device is available")
    }
   //...
}

AVCaptureDeviceInput 설정

생성한 AVCaptureDevice를 기반으로 AVCaptureDeviceInput을 생성합니다

mutating func configureCamera(cameraPosition: AVCaptureDevice.Position) {
    //...
        
    do {
        if let currentInput = currentInput { // 카메라 전환시 기존 연결 해제
            self.captureSession.removeInput(currentInput)
        }
        let deviceInput = try AVCaptureDeviceInput(device: cameraDevice)
        self.captureSession.addInput(deviceInput)
        self.currentInput = deviceInput
    } catch {
        print("error = \(error.localizedDescription)")
    }
}

카메라 권한 확인 메서드 생성 및 info.plist 작성

카메라 기능을 사용하려면 사용자에게 허락을 받아야 합니다

카메라 권한 확인 메서드

func requestCameraPermission() {
    switch AVCaptureDevice.authorizationStatus(for: .video) {
    case .notDetermined:
       AVCaptureDevice.requestAccess(for: .video) { authStatus in
            if authStatus {
                self.cameraQueue.async {
                    self.captureSession.startRunning() // 카메라 시작
                }
            }
        }
    case .restricted:
        break
    case .authorized:
        self.cameraQueue.async {
            self.captureSession.startRunning() // 카메라 시작
        }
    default:
        print("Permession declined")
    }
}

info.plist

key: Privacy - Camera Usage Description (100% 똑같아야 합니다)
Value: Use camera (마음대로 입력해도 됩니다)

PreviewView 합치기

ContentView에서 cameraPreviewView를 사용합니다

import SwiftUI
import AVFoundation

struct ContentView: View {
    @State var cameraPosition: AVCaptureDevice.Position = .back
    @State var cameraPreviewView: CameraPreviewView
    
    var body: some View {
        ZStack {
            cameraPreviewView
                .ignoresSafeArea()
                .onAppear(perform: {
                    cameraPreviewView.requestCameraPermission()
                })
            VStack {
                Spacer()
                Spacer()
                HStack {
                    Spacer()
                    Spacer()
                    Button(action: {
                        let newPosition = cameraPosition == .back ? AVCaptureDevice.Position.front : .back
                        cameraPosition = newPosition
                        cameraPreviewView.configureCamera(cameraPosition: newPosition)
                    }) {
                        Image(systemName: "arrow.triangle.2.circlepath.camera")
                            .resizable()
                            .scaledToFit()
                            .frame(width: 50, height: 50)
                    }.padding(.trailing, 20)
                }
            }
        }
    }
}

완성

전체 코드

import SwiftUI
import AVFoundation

struct ContentView: View {
    @State var cameraPosition: AVCaptureDevice.Position = .back
    @State var cameraPreviewView: CameraPreviewView
    
    var body: some View {
        ZStack {
            cameraPreviewView
                .ignoresSafeArea()
                .onAppear(perform: {
                    cameraPreviewView.requestCameraPermission()
                })
            VStack {
                Spacer()
                Spacer()
                HStack {
                    Spacer()
                    Spacer()
                    Button(action: {
                        let newPosition = cameraPosition == .back ? AVCaptureDevice.Position.front : .back
                        cameraPosition = newPosition
                        cameraPreviewView.configureCamera(cameraPosition: newPosition)
                    }) {
                        Image(systemName: "arrow.triangle.2.circlepath.camera")
                            .resizable()
                            .scaledToFit()
                            .frame(width: 50, height: 50)
                    }.padding(.trailing, 20)
                }
            }
        }
    }
}

import Foundation
import AVFoundation
import SwiftUI

struct CameraPreviewView: UIViewRepresentable {
    init(cameraPosition: AVCaptureDevice.Position) {
        configureCaptureSession(cameraPosition: cameraPosition)
    }
    
    func makeUIView(context: Context) -> UIView {
        let view = PreviewView()
        view.backgroundColor = .black
        if let previewLayer = view.previewLayer {
            previewLayer.session = captureSession
            previewLayer.videoGravity = .resizeAspectFill
            previewLayer.connection?.videoRotationAngle = 90.0
        }
        return view
    }
    
    func updateUIView(_ uiView: UIView, context: Context) {
        // do nothing
    }
    
    func requestCameraPermission() {
        switch AVCaptureDevice.authorizationStatus(for: .video) {
        case .notDetermined:
            AVCaptureDevice.requestAccess(for: .video) { authStatus in
                if authStatus {
                    self.cameraQueue.async {
                        self.captureSession.startRunning()
                    }
                }
            }
        case .restricted:
            break
        case .authorized:
            self.cameraQueue.async {
                self.captureSession.startRunning()
            }
        default:
            print("Permession declined")
        }
    }
    
    private let cameraQueue = DispatchQueue(label: "cameraQueue")
    private var captureSession = AVCaptureSession()
    private mutating func configureCaptureSession(cameraPosition: AVCaptureDevice.Position) {
        let videoOutput = AVCaptureVideoDataOutput()
        self.captureSession.addOutput(videoOutput)
        
        configureCamera(cameraPosition: cameraPosition)
    }
    
    private var currentInput: AVCaptureDeviceInput?
    
    mutating func configureCamera(cameraPosition: AVCaptureDevice.Position) {
        let discoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera,.builtInUltraWideCamera], mediaType: .video, position: cameraPosition)
        
        guard let cameraDevice = discoverySession.devices.first else {
            fatalError("no camera device is available")
        }
        
        do {
            if let currentInput = currentInput {
                self.captureSession.removeInput(currentInput)
            }
            let deviceInput = try AVCaptureDeviceInput(device: cameraDevice)
            self.captureSession.addInput(deviceInput)
            self.currentInput = deviceInput
        } catch {
            print("error = \(error.localizedDescription)")
        }
    }
}

fileprivate class PreviewView:UIView {
    override class var layerClass: AnyClass {
        return AVCaptureVideoPreviewLayer.self
    }
    
    var previewLayer: AVCaptureVideoPreviewLayer? {
        return layer as? AVCaptureVideoPreviewLayer
    }
}

시연

전면까지 찍어야 하다보니 마땅한 곳이 없네요...ㅎ

참고

https://ios-development.tistory.com/1043
https://enebin.medium.com/swiftui만-써서-호다닥-카메라앱-만들기-feat-mvvm-1-2782b457f796

2개의 댓글

comment-user-thumbnail
2024년 4월 13일

카메라 출력부를 레이어만 뜯어 화면에 띄울 수 있는걸 처음 알았어요..! 항상 뷰컨을 래핑해 스유에서 띄웠었는데 간단한 화면정도라면 uiview로부터 가져오면 되는군요!! 오늘도 하나 배워갑니다..ㅎㅎㅎ

1개의 답글