HIT해

[SwiftUI] 카메라 앱 기능 구현하기 (AVFoundation) 본문

Swift/Swift 개발 노트

[SwiftUI] 카메라 앱 기능 구현하기 (AVFoundation)

힛해 2024. 3. 19. 23:59
728x90


SwiftUI로 카메라 기능을 구현하기위해서는 크게 4가지가 필요하다.
1. 카메라 설정을 하기위한 클래스

2. 카메라 버튼을 담을 뷰

3. 실시간으로 카메라의 정보가 담기는 뷰

4. ZStack으로 카메라의 정보와 버튼이 화면에 표시되는 뷰
+ info custom iOS Target Properties 설정

Privacy - Camera Usage Description을 추가해준다.

 

이제 개발을 시작하자

 

1 카메라 설정을 하기위한 Service Class

import Foundation
import AVFoundation

class CameraService {
    
    // 전체에 적용되는 세션
    var session : AVCaptureSession?
    // 속성 설정,캡처 동작을 구성하고 입력 장치의 데이터 흐름을 조정하여 출력을 캡처하는 개체입니다.
    var delegate : AVCapturePhotoCaptureDelegate? // 진행 상황을 모니터링하고 사진 캡처 출력에서 ​​결과를 수신하는 방법입니다.
    
    // 카메라 뷰에 넣을 대리인
    let output = AVCapturePhotoOutput()
    // 미리 보기 레이어, 이 미리보기 레이어가 UIViewController에 추가될 예정
    let previewLayer = AVCaptureVideoPreviewLayer()
    
    // 여기까지 캡쳐 서비스에 필요한 조건들
    
    // 실행시켰을떄 권한을 확인하기 위함
    func start(delegate: AVCapturePhotoCaptureDelegate, completion: @escaping(Error?) -> ()){
        self.delegate = delegate // 권한을 확인하려고함
        checkPermission(completion: completion)
    }
    
    private func checkPermission(completion: @escaping(Error?)->()){
        
        // 비디오에 대한 승인 상태를 확인
        switch AVCaptureDevice.authorizationStatus(for: .video){
            
        case .notDetermined:
            // 권한을 요청함
            AVCaptureDevice.requestAccess(for: .video){ [weak self] granted in // 백그라운드 스레드 내부에 있기에 weak self 추가
                guard granted else { return }
                // 권한설정이 true면 아래의 코드 실행
                DispatchQueue.main.async {
                    self?.setupCamera(completion: completion)
                }
            }
        case .restricted:
            break
        case .denied:
            break
        case .authorized: 
            // 승인된 상태일때 카메라 시작
            setupCamera(completion: completion)
        @unknown default:
            break
        }
    }
    
    
    // 카메라 설정
    private func setupCamera(completion: @escaping(Error?)->()){
        // 전체 적용 세션에 담기 위한 작업들
        let session = AVCaptureSession()
        // 기기 기본값이 존재하면 실행
        if let device = AVCaptureDevice.default(for: .video){
            do{
                let input = try AVCaptureDeviceInput(device: device)
                if session.canAddInput(input){
                    session.addInput(input)
                    /*
                     입력 구성
                     var inputs: [AVCaptureInput]
                     캡처 세션에 미디어 데이터를 제공하는 입력입니다.
                     func canAddInput(AVCaptureInput) -> Bool
                     세션에 입력을 추가할 수 있는지 여부를 결정합니다.
                     func addInput(AVCaptureInput)
                     세션에 캡처 입력을 추가합니다.
                     func removeInput(AVCaptureInput)
                     세션에서 입력을 제거합니다.
                     */
                }
                
                if session.canAddOutput(output){
                    session.addOutput(output)
                    /*
                     출력 구성
                     var outputs: [AVCaptureOutput]
                     캡처 세션이 데이터를 보내는 출력 대상입니다.
                     func canAddOutput(AVCaptureOutput) -> Bool
                     세션에 출력을 추가할 수 있는지 여부를 결정합니다.
                     func addOutput(AVCaptureOutput)
                     캡처 세션에 출력을 추가합니다.
                     func removeOutput(AVCaptureOutput)
                     캡처 세션에서 출력을 제거합니다.
                     */
                }
                
                
                // previewLayer 의 크기를 설정
                previewLayer.videoGravity = .resizeAspectFill
                previewLayer.session = session
                
                // session을 실행시킨다.
                session.startRunning()
                // 전체 session에 담는다.
                self.session = session
                
            }catch{
                completion(error)
            }
        }
    }
    
    // setting을 기본값으로 해준다.
    func capturePhoto(with settings : AVCapturePhotoSettings = AVCapturePhotoSettings()){
        // 기본 셋팅과 대리인을 전달한다
        output.capturePhoto(with: settings, delegate: delegate!)
    }
    // 카메라 버튼을 눌렀을때 실행되는 함수
    
}

AVCaptureSession

An object that configures capture behavior and coordinates the flow of data from input devices to capture outputs.


https://developer.apple.com/documentation/avfoundation/avcapturesession

 

AVCaptureSession | Apple Developer Documentation

An object that configures capture behavior and coordinates the flow of data from input devices to capture outputs.

developer.apple.com

 

AVCapturePhotoCaptureDelegate

Methods for monitoring progress and receiving results from a photo capture output.

 

AVCaptureSession은 앱 내부에서 데이터의 흐름을 관리하는 역할을 담당하고

AVCapturePhotoCaptureDelegate는 데이터 전달자라고 생각하면 된다.

2. 카메라 기능을 실행시키기위한 뷰

struct ContentView: View {

    @State private var capturedImage : UIImage? = nil
    @State private var isCustomCameraViewPresented = false
    
    var body: some View {
        ZStack{
            if capturedImage != nil {
                Image(uiImage: capturedImage!).resizable().scaledToFill().ignoresSafeArea()
            }else{
                Color(UIColor.systemBackground)
            }
            VStack{
                Spacer()
                Button(action: {isCustomCameraViewPresented.toggle()}, label: {Image(systemName: "camera.fill").font(.largeTitle).padding().background(Color.black).foregroundColor(.white).clipShape(Circle())}).padding(.bottom).sheet(isPresented: $isCustomCameraViewPresented, content: {CustomCameraView(capturedImage: $capturedImage)})
                // 실시간으로 카메라 영상이 보이는 화면 표시
            }
        }
        
    }
}

@State UIImage를 선언해 사진을 받을 변수를 준비한다.

 

3. 실시간으로 카메라의 정보가 담기는 뷰

import SwiftUI
import AVFoundation

// 버튼 뒤에 나오는 실시간 화면
struct CameraView : UIViewControllerRepresentable {
   
    
    typealias UIViewControllerType = UIViewController
    
    // 카메라 보기 외부에서 캡처 사진에 엑세스하기를 원하기 때문에 카메라 서비스를 초기화해줘야함
    // 그래야 버튼등을 내가 원하는대로 커스텀할 수 있다.
    let cameraService : CameraService
    
    // 결과값이 오류가 나올 수도 있기에 이렇게 설정
    let didFinishProcessingPhoto: (Result<AVCapturePhoto, Error>) -> ()
    
    //UIView를 만듬
    func makeUIViewController(context: Context) -> UIViewController {
        cameraService.start(delegate: context.coordinator){ err in
            if let err = err {
                didFinishProcessingPhoto(.failure(err))
                return
            }
        }
        
        // ViewController 선언
        let viewController = UIViewController()
        
        viewController.view.backgroundColor = .blue
        viewController.view.layer.addSublayer(cameraService.previewLayer)
        // 이전에 클래스에 선언해둔 previewLayer
        cameraService.previewLayer.frame = viewController.view.bounds
        return viewController
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self, didFinishProcessingPhoto: didFinishProcessingPhoto)
    }
    
    
    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
        
    }
    
    // 실제로 원하는 대리자를 준수하게 된다.
    class Coordinator : NSObject,AVCapturePhotoCaptureDelegate {
        
        // 부모 뷰를 추가
        let parent : CameraView
        private var didFinishProcessingPhoto: (Result<AVCapturePhoto, Error>) -> ()
        
        // 부모 뷰를 위한 초기화
        // @escaping을 사용하는 이유는 함수가 끝나고도 사용을 원할때 ex) 스크린샷을 찍고 잠깐 남아있는 경우가 그 예시
        init(_ parent: CameraView, didFinishProcessingPhoto: @escaping (Result<AVCapturePhoto, Error>) -> ()){
            self.parent = parent
            self.didFinishProcessingPhoto = didFinishProcessingPhoto
        }
        
        
        func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?){
            if let error = error {
                didFinishProcessingPhoto(.failure(error))
                return
            }
            didFinishProcessingPhoto(.success(photo))
            
        }
        
        
    }
    
}

이부분에서 UIViewController는 SwiftUI가 아닌 UIKit 사용법을 숙지해야한다.

위와같이 Service클래스에 구현한 기능을 불러와 화면을 구성한다.

 

4. 버튼과 실시간 화면이 겹쳐져있는 ZStack뷰

import SwiftUI

struct CustomCameraView : View
{
    let cameraService = CameraService()
    @Binding var capturedImage : UIImage?
    
    @Environment(\.presentationMode) private var presentationMode
    
    var body: some View{
        ZStack{
            CameraView(cameraService: cameraService) { result in
                switch result{
                case .success(let photo):
                    if let data = photo.fileDataRepresentation(){
                        // 사진 버튼을 누르면 아래와 같이 동작
                        capturedImage = UIImage(data : data)
                        presentationMode.wrappedValue.dismiss()
                    }else{
                        print("Error : no image data found")
                    }
                case .failure(let err):
                    print(err.localizedDescription)
                }
            }
            VStack{
                Spacer()
                Button(action: {
                    cameraService.capturePhoto()
                }, label: {Image(systemName: "circle").font(.system(size: 72)).foregroundColor(.white)})
            }
        }
    }
}

 

버튼을 눌러 cameraService의 capturePhoto가 실행되면

let output = AVCapturePhotoOutput()

output.capturePhoto함수와 함께 기본 셋팅과 delegate가 전달되어 성공코드와 함께 사진이 전달되고
화면이 닫히며 이전 페이지에 바인딩 해둔 UIImage에 값이 할당되게 된다.