HIT해
[SwiftUI] 카메라 앱 기능 구현하기 (AVFoundation) 본문
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
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
https://developer.apple.com/documentation/avfoundation/avcapturephotocapturedelegate
AVCapturePhotoCaptureDelegate | Apple Developer Documentation
Methods for monitoring progress and receiving results from a photo capture output.
developer.apple.com
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에 값이 할당되게 된다.
'Swift > Swift 개발 노트' 카테고리의 다른 글
[상추] 개인정보처리방침 (0) | 2024.03.28 |
---|---|
[SwiftUI] 배경 그라데이션 적용하기 (0) | 2024.03.20 |
[SwiftUi] Error Handling - sheet presentationDetents 적용해결 (0) | 2024.03.15 |
[SwiftUi] List 알아보기 (0) | 2024.03.14 |
[SwiftUi] SwiftData 이해하기 - 2 (0) | 2024.03.12 |