✅ 이번시간에는 FullScreen 카메라 앱에 대해서 알아볼 생각이야.
FullScreen 카메라는 생각보다 많이 어렵다.. 기존에 시스템 카메라를 이용하는 서비스를 만들기도 했었는데, 카메라 자체를 만들어서 쓰는 것또한 코드가 완전히 달라, 쉽지 않은 과정이었던 것 같아.
✊ 그래서 이번시간에는 그 어떤 시간보다 더 꼼꼼하게 학습해서 나만의 카메라 앱을 시장에 내놓아 보자!!
코드리뷰에 사용한 코드주소
https://github.com/lgvv/fastCampus/tree/main/FullScreenCamera
우선 시작하기에 앞서
다이어그램을 이해하기 보다 간단하게만 보고 넘어가자!! 이런 구조라는 것 정도만!
자 그럼 천천히 코드 리뷰를 진행해 볼까?
(목차)
1. 카메라 초기 설정(시스템 카메라 X)
2. setupSession에 대해서 알아보자
- ❗️세션을 시작하는 것도 잊지말자!
3. capturePhoto 버튼을 누르면 사진을 찍어야겠지? 이건 어떻게 작동하는걸까
4. 사진을 찍었으면 후보정? 그건 어떻게 처리하는건데?
5. 사진찍고 후보정도 했어, 그럼 이번에는 사진을 저장해볼까?
6. 전면, 후면 카메라 등 전환할 때의 코드
7. 코드에 리터럴(literal)을 사용하는 법 + setupUI()
8. preview.swift 파일에 대해서 알아볼까?
✅ 1. 카메라 초기 설정(시스템 카메라 X)
시스템 카메라와 별개로 다른 프로세스 과정을 진행하게 되는데 여기서는 큰 흐름에 대해서 하나하나 짚어볼 생각이야.
- CaptureSession
- AVCaptureDeviceInput
- AVCapturePhotoOutput
- Queue
- AVCaptureDeviceDiscoverySession
이렇게 크게 5가지 흐름을 갖게 돼.
아래의 코드를 한번 봐 볼까?
let captureSession = AVCaptureSession()
var videoDeviceInput : AVCaptureDeviceInput! // 후면, 전면으로 카메라 전환할수도 있으니까
let photoOutput = AVCapturePhotoOutput()
let sessionQueue = DispatchQueue(label: "session Queue")
let videoDeviceDiscoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInDualCamera, .builtInWideAngleCamera, .builtInTrueDepthCamera], mediaType: .video, position: .unspecified)
// 디바이스 타입에는 아이폰이 카메라 갯수가 여러개가 되면서 이걸 사용하는게 조금씩 달라졌다.
세션을 만들고 - 인풋 디바이스를 만든 후 - 아웃풋에 대한 것도 만든다. 그리고 카메라를 실행할 큐를 따로 설정해준 뒤, 마지막으로 실제 기기에 파라미터를 통해 세팅해주는 작업으로 진행 돼.
✅ 그럼 AVCaptureDeviceDiscoverySession의 파라미터에 대해서 한번 살펴 볼까?
- 파라미터에 대한 부분은 맨 아래 애플 공식문서도 함께 넣어두었으니 꼭 참고할 것
공식 문서의 Overview 부분을 한글로 번역해 보자면
" 이 클래스를 사용하여 특정 장치 유형 (예 : 마이크 또는 광각 카메라), 캡처에 지원되는 미디어 유형 (예 : 오디오, 비디오 또는 둘 다) 및 위치 (전면 또는 후면)와 일치하는 사용 가능한 모든 캡처 장치를 찾습니다. ).
장치 검색 세션을 생성 한 후 devices어레이를 검사하여 캡처 할 장치를 선택할 수 있습니다. "
(구글 번역기)
1️⃣ deviceType : 아이폰 후면에 달린 카메라 중 어떤 카메라 쓸건지 배열로 지정해준다.
이게 무슨 말이냐면, 아이폰이 카메라를 1개가 아닌 여러개를 갖게 되면서 어떤 카메라를 사용할건지 지정해 주어야 한다. 사용할 카메라를 배열에 넣으면 되는거라 크게 어렵지 않다.
2️⃣ mediaType : 공식 문서의 설명을 보면 "캡쳐할 미디어의 유형" 이래 audio도 있고 그런데, 우리는 사진을 사용할 것임으로 media를 넣어서 사용할 예정이야
3️⃣ position : "시스템 하드웨어 (전면 또는 후면)를 기준으로 검색 할 캡처 장치의 위치" 라고 설명해주고 전면, 후면 상관없이 사용하려면
파라미터의 값으로 .unspecified을 넣어주면 돼. 전면은(.front) 후면은(.back)를 사용한다!
✅ 2. setupSession에 대해서 알아보자
초기설정이 끝났으면 그 다음으로 알아봐야 하는 것은 위에서 보았다 싶이 세션에 대해서 먼저 세팅을 해줘야겠지?
이 부분은 코드로 함께 볼건데, 길고 복잡하니 꼼꼼하게 읽어보자
func setupSession() {
// TODO: captureSession 구성하기
// - presetSetting 하기 : 미디어 캡쳐를 할때, 사진을 찍을수도 있고, 영상을 찍을수도 있고 해상도를 정할수도 있고 등등 하니까 그에 대한 것을 먼저 설정해줘야 한다.
// - beginConfiguration
// - Add Video Input
// - Add Photo Output
// - commitConfiguration
captureSession.sessionPreset = .photo // 사진을 찍을거니까 포토로!
captureSession.beginConfiguration() // 나 이제 구성 시작할거라고 피시에게 알림
// - Add Video Input
do {
var defaultVideoDevice : AVCaptureDevice?
if let dualCameraDevice = AVCaptureDevice.default(.builtInDualCamera, for: .video, position: .back) {
defaultVideoDevice = dualCameraDevice
} else if let backCameraDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) {
defaultVideoDevice = backCameraDevice
} else if let frontCameraDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) {
defaultVideoDevice = frontCameraDevice
}
guard let camera = videoDeviceDiscoverySession.devices.first else {// 여기에서 찾은게 있으면 첫번째꺼 가져옴 - 핸드폰에서 카메라를 찾을떄, videoDeviceDiscoverySession 중 제일 마지막 파라미터에 명시된 부분을 찾으라는 의미
captureSession.commitConfiguration() // 못찾았으면 그냥 종료하게끔
return
}
let videoDeviceInput = try AVCaptureDeviceInput(device: camera)
if captureSession.canAddInput(videoDeviceInput) {
captureSession.addInput(videoDeviceInput)
self.videoDeviceInput = videoDeviceInput
} else {
captureSession.commitConfiguration()
return
}
} catch let error{
captureSession.commitConfiguration()
return
}
// -Add photo Output
photoOutput.setPreparedPhotoSettingsArray([AVCapturePhotoSettings(format: [AVVideoCodecKey : AVVideoCodecType.jpeg])], completionHandler: nil) // 사진을 어떤 형식으로 저장할지 미리 세팅해두기
if captureSession.canAddOutput(photoOutput) {
captureSession.addOutput(photoOutput)
} else {
captureSession.commitConfiguration()
return
}
captureSession.commitConfiguration() // 구성 완료했다고 알림
}
위에 보다시피 코드가 길고 어려워 보이지?
✅ 캡쳐 세션 구성에서 5가지 흐름으로 구성이 되는데 한번 볼까? (captureSession 구성하기)
- presetSetting : 미디어 캡쳐를 할때, 사진을 찍을수도 있고, 영상을 찍을수도 있고 해상도를 정할수도 있고 등등 하니까 그에 대한 것을 먼저 설정해줘야 한다.
- beginConfiguration : 실행중인 세션에서 여러 구성 작업을 배치, commitConfiguration 실행되기 전까지 변경사항이 적용되지 않음
- Add Video Input :
- Add Photo Output :
- commitConfiguration : beginConfiguration과 이 코드 사이에서 설정된 변경사항을 실제로 적용 시켜줌
코드에 대해서 살펴보면, 코드에서도 사진을 사용할 예정이라 sessionPreset 부분에 photo를 넣어주고, 그 다음에 beginConfiguration을 하는 모습을 확인할 수 있어.
그 다음은 세션을 정해주고 실제로 어떤 카메라를 쓸건지 알 수 있어야 하잖아?
AVCaptureDevice.default에 대한 공식문서의 설명을 보면
"지정된 장치 유형, 미디어 유형 및 위치에 대한 기본 장치를 반환합니다."
라고 말해주고 있어 -> 이 부분은 공식문서를 쓱 읽어보면 아주 이해가 잘되니까 직접 찾아봐도 좋을거 같아.
그 다음이 guard문인데, 디바이스에서 카메라를 찾을건데, 디바이스에서 카메라가 여러개가 검색될 수도 있어. (전면, 후면-듀얼 등) 그래서 우리는 first를 통해 카메라 중 하나를 이용해
그 다음은 captureDeviceInput에 대한건데, 이것도 공식문서를 참고로 넣어 두었어
공식문서의 설명에 따르면
"캡처 세션에 대한 입력 (예 : 오디오 또는 비디오)을 제공하고 하드웨어 별 캡처 기능에 대한 제어를 제공하는 장치입니다."
라고 언급되어 있어.
그러니까 여기까지 정리해보자면
1. AVCaptureDevice 통해 디바이스 카메라의 어떤 부분 쓸건지 지정해주고
2. videoDeviceDiscoverySession 통해서 1의 과정에서 설정한 카메라를 우리 디바이스에서 실제로 찾아서 사용할 수 있는지 정보를 확인해준 뒤
3. AVCaptureDeviceInput 통해서 실제 기기에서도 사용 가능하면, 우리가 인풋으로 받아들일 카메라를 지정해주는 것까지야
그 이후로는 CanAddInput을 통해 세션에 넣을 수 있는지 물어본 다음에 (실패할 수 있는 작업이여서 먼저 물어보는게 우선)
우리가 만들어 둔 캡쳐세션에 카메라를 붙인다!
다음은 photoOut에 대한 부분인데 이 부분은 사진을 찍었으면 이제 어디로 내보내야할거 아냐 그치?
우리가 찍은사진의 포맷을 지정한 후 내보내주면 끝!
🔸 세션을 이용할 때, 비긴을 세션에 대한 변경을 저장하는 거고 세션을 꼭 isRunning이나 isStop을 통해 확인하고 실행해주어야 사용할 수 있다.
✅ 3. capturePhoto 버튼을 누르면 사진을 찍어야겠지? 이건 어떻게 작동하는걸까
미리 알고가자! preview는 이렇게 구조를 가진다.
@IBAction func capturePhoto(_ sender: UIButton) {
// TODO: photoOutput의 capturePhoto 메소드
// orientation
// photooutput
let videoPreviewLayerOrientation = self.previewView.videoPreviewLayer.connection?.videoOrientation // 사진을 뒤집어서 찍을 수도 있는데, 현재 프리뷰레이어가 갖고 있는걸 그대로 적용한다.
sessionQueue.async {
// 미디어에서 들어온 데이터가 사진이 되어서 바깥으로 나가야하는데
let connection = self.photoOutput.connection(with: .video) // 아웃풋 결과를 커넥션을 통해 연결
connection?.videoOrientation = videoPreviewLayerOrientation! // 사진에 대한 오리엔테이션을 설정
let setting = AVCapturePhotoSettings() // 캡쳐포토 세팅.
self.photoOutput.capturePhoto(with: setting, delegate: self) // 사진찍자고 포토 아웃풋에 알려주는거
// 사진 찍을때, 후처리를 해줄 수 있는데 델리게이트 셀프로 해서 여러가지 메소드가 있는데 그걸로 해결해주기
}
}
위의 코드를 보자.
사진을 찍었을 때, 우리는 우선 Orientation 부분에 대해서 알아보아야 해.
Orientation 부분은 쉽게 말해서 우리가 화면을 뒤집어서 찍을 수도 있을거 아니야? 그에 대한 처리르 어떻게 할 것인지에 대한 명시하는 부분이야.
저기 설명을 보면
"캡처 된 비디오를 표시하는 핵심 애니메이션 레이어입니다."
라고 설명하고 있다. 그만큼 중요하다는 말!! 이건 참고 부분에 넣어 두었는데 공식문서를 읽어가면서 코드를 읽어보면 쉽게 이해할 수 있어.
Preview쪽도 코드로 구현해 두었는데, 잠시 후에 보기로 하고 우선 이 코드에 맞게끔 알아보자.
저기 보면 connection이라는 코드가 보이지? 저건 우리가 포토 아웃풋으로 찍은 결과를 커넥션을 통해 연결하는 코드야.
이 부분도 참고쪽에 넣어두었으니 공식 문서를 확인해 보길!!
그 다음은 오리엔트를 설정하는 부분인데 사진이 뒤돌아 있을 수도 있는데, 이걸 어떻게 설정해줄지에 대한 코드야.
그 다음은 AVCapturePhotoSettings() 인데 이건 사진을 찍은 후에 어떤 작업(후보정)을 해줄건지에 대한 정보를 줄 수 있는 코드야.
즉, 우리가 찍은 사진에 대해서 필터를 씌우거나 워터마크 등을 줄때, 저렇게 처리를 해.
delegate 부분은 extension으로 처리하는데 조금 이따가 보자.
그럼 다시 정리하자면,
오리엔트를 통해 preview에 우리가 보여줄 화면을 알려주고
사진을 찍은 결과는 후보정을 통해 사용한다.
✅ 4. 사진을 찍었으면 후보정? 그건 어떻게 처리하는건데?
extension CameraViewController: AVCapturePhotoCaptureDelegate {
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) { // 사진이 다 찍혔을때, 후처리
// TODO: capturePhoto delegate method 구현
guard error == nil else { return }
guard let imageData = photo.fileDataRepresentation() else { return }
guard let image = UIImage(data: imageData) else { return }
self.savePhotoLibrary(image: image)
}
}
위에서 delegate를 연결한 부분은 이렇게 사용할 수 있어.
카메라 후보정과 관련한 메소드인데, 우리가 사진 찍기 버튼을 누르고, 이런 로직에 의해서 실행 돼.
자세한 정보는 공식문서를 읽어보면 쉽게 이해할 수 있어.
저기 가면 라이브 포토, 영상 등 로직이 조금씩 다른데 읽어보기.
우리는 didFinishProcessingPhoto 메소드를 이용할 예정인데 guard문을 몇몇 조건을 처리한 후 넘어가
fileDataRepresentation 코드의 경우에는 애플 공식 문서에 따르면
"사진 및 첨부 파일의 플랫 데이터 표현을 생성하고 반환합니다."
라고 말하고 있어.
자세한건 공식문서를 더 찾아보면 도움이 되겠지!!
정리하자면,
1. photo 타입이 AVCapturePhoto 타입이라서
2. fileDataRepresentation 통해 Data 타입으로 변경
3. UIImage를 사용해서 라이브러리에 저장할 수 있는 이미지로 변경
4. 그 이후에 저장!! 이런 로직이야
✅ 5. 사진찍고 후보정도 했어, 그럼 이번에는 사진을 저장해볼까?
func savePhotoLibrary(image: UIImage) {
// TODO: capture한 이미지 포토라이브러리에 저장
PHPhotoLibrary.requestAuthorization { status in
if status == .authorized {
// 저장
PHPhotoLibrary.shared().performChanges {
PHAssetChangeRequest.creationRequestForAsset(from: image) // 이미지를 포토 라이브러리에 생성하겠다
} completionHandler: { (success, error) in
print("이미지 저장 완료 --> \(success)")
}
} else {
// 다시 요청
print("--> 권한을 얻지 못함")
}
}
}
사진을 우리의 갤러리에 저장해야겠지?
저장하기 전에는 항상 권한을 얻어야 하는데, 권한을 확인한 후에 저장하는 방법은 다음과 같아.
여기서 권한이 조금 신경쓰이는 것이, 우리가 시스템 카메라를 사용할때는 저장하는 방법이 이와 달랐는데, 실제로 권한을 얻는 부분에 있어서 이렇게 사용한다는 것을 알아두자!
이 코드는 애플 공식 문서에 따르면 shared는 공유 사진 저장소에 대한 싱글톤 표현이며, performChages 사진 라이브러리에 대한 변경을 비동기로 요청하는 블록이며, PHAssetChangeRequest는 사진 라이브러리의 생성, 삭제, 변경, 편집을 담당하고 있으며 그중 creationRequestForAsset을 통해 생성을 담당한다.
애플 공식 문서에 따르면 라이브러리의 권한 및 액세스 변경을 관리하는 라이브러리라고 한다.
✅ 6. 전면, 후면 카메라 등 전환할 때의 코드
여기에는 전면부 후면부 사용에 따라 카메라를 설정하는 부분이야.
@IBAction func switchCamera(sender: Any) {
// TODO: 카메라는 1개 이상이어야함
guard videoDeviceDiscoverySession.devices.count > 1 else {
return
}
// TODO: 반대 카메라 찾아서 재설정
// - 반대 카메라 찾고
// - 새로운 디바이스 가지고 세션 업데이트
// - 카메라 토글 버튼 업데이트
sessionQueue.async {
let currentvideoDevice = self.videoDeviceInput.device // 현재 카메라가 뭔지 알아야지
let currentPosition = currentvideoDevice.position // 카메라가 앞인지 뒤인지도 알아야지
let isFornt = currentPosition == .front // 전면 카메라 인가요?
let preferredPosition : AVCaptureDevice.Position = isFornt ? .back : .front
let devices = self.videoDeviceDiscoverySession.devices
var newVideoDevice : AVCaptureDevice?
newVideoDevice = devices.first(where: { device in
return preferredPosition == device.position
})
// update capture session
if let newDevice = newVideoDevice {
do {
let videoDeviceInput = try AVCaptureDeviceInput(device: newDevice)
self.captureSession.beginConfiguration()
self.captureSession.removeInput(self.videoDeviceInput)
// add new device input
if self.captureSession.canAddInput(videoDeviceInput) {
self.captureSession.addInput(videoDeviceInput)
self.videoDeviceInput = videoDeviceInput
} else {
self.captureSession.addInput(videoDeviceInput)
}
self.captureSession.commitConfiguration()
DispatchQueue.main.async {
self.updateSwitchCameraIcon(position: preferredPosition) // 이 포지션에 따라서 아이콘을 업데이트 시킨다는 이야기
}
} catch let error{
print(" error occured while creating device input : \(error.localizedDescription)")
}
}
}
}
우선 세션에 카메라가 1개 초과(전면, 후면) 즉, 최소 2개는 있어야겠지? - 주석이 조금 잘못달림.
현재 사용하는 카메라가 어떤건지 알아내야지 코드를 쭉 읽어보면 쉽진 않지만 이해할 수 있어.
전,후면 전환시에는 현재 붙어있는 카메라를 지운뒤에 다시 add해야 해
✅ 7. 코드에 리터럴(literal)을 사용하는 법 + setupUI()
이미지랑 컬러 등에 사용할 수 있는데, #image literal 또는 #color literal 을 사용하면 저렇게 네모 모양으로 바뀌고 저 네모 모양을 더블 클릭해서 변경할 수가 있다는 장점이 있어..!
setupUI() 쪽은 둥글게 만들기 위한 코드인데, 이건 직접 찾아보자
✅ 8. preview.swift 파일에 대해서 알아볼까?
// Preview.swift
class PreviewView: UIView {
var videoPreviewLayer: AVCaptureVideoPreviewLayer {
guard let layer = layer as? AVCaptureVideoPreviewLayer else {
fatalError("Expected `AVCaptureVideoPreviewLayer` type for layer. Check PreviewView.layerClass implementation.")
}
layer.videoGravity = AVLayerVideoGravity.resizeAspectFill
layer.connection?.videoOrientation = .portrait
return layer
}
var session: AVCaptureSession? {
get {
return videoPreviewLayer.session
}
set {
videoPreviewLayer.session = newValue
}
}
// MARK: UIView
override class var layerClass: AnyClass {
return AVCaptureVideoPreviewLayer.self
}
}
아까도 계속 언급했었는데, 이제야 여기를 보기로 해요.
이부분은 오리엔트 관련한 부분인데, 강의에서는 따로 설명이 없었어. 그런데 정리를 하지 않고 넘어갈 수는 없겠지?
AVCaptureSession 부분은 미리 본 캡쳐 세션 부분이다.
"AVCaptureVideoPreviewLayer is a subclass of CALayer that you use to display video as it’s captured by an input device."
쉽게 말해서 입력 장치에서 캡처 한 비디오를 표시하는 데 사용하는 CALayer의 하위 클래스란 의미이다.
다음 코드를 보면 videoGravity 부분을 볼 수 있는데, 레이어가 화면 내에서 컨텐츠를 표시하는 방법에 대한 부분이다.
근데 이게 default 값이 resizeAspect라서 바꿔준 모습이다.
그리고 .portrait이 보이지? 저건 어떻게 연결되어 있냐? 이런 말인데, 솔직히 와닿지는 않는데, 저걸 작성하면 휴대폰을 가로로 찍어도 갤러리에 가로로 저장되지, 굳이 가로로 찍은 사진을 세로로 돌려서 갤러리에 저장하지 않는다는 말 같다.(추측)
그리고 마지막에
override class var layerClass: AnyClass {
return AVCaptureVideoPreviewLayer.self
}
이 부분은 구글링 해본 결과 사용자가 입력을 볼 수 있게 해주는 매우 중요한 코드라고 한다.
후,, 우선 이정도로 코드리뷰를 마치기로 하고, 강의에서는 Learning AV Foundation 책을 추천해 주셨는데, 깃허브에 다른 분이 먼저 공부하고 남긴 글이 있어 이걸 참고한다면 더 확장할 수 있지 않을까 싶다.
https://github.com/Kiboom/AVFoundation
(참고)
https://developer.apple.com/documentation/avfoundation/avcapturedevice/discoverysession
https://developer.apple.com/documentation/avfoundation/avcapturedevice/discoverysession/2361539-init
https://developer.apple.com/documentation/avfoundation/avcapturedevice
https://developer.apple.com/documentation/avfoundation/avcapturedeviceinput
https://developer.apple.com/documentation/avfoundation/avcapturevideopreviewlayer
https://developer.apple.com/documentation/avfoundation/avcapturevideopreviewlayer/1390893-connection
https://developer.apple.com/documentation/avfoundation/avcapturephotocapturedelegate
https://developer.apple.com/documentation/avfoundation/avcapturephoto/2873919-filedatarepresentation
https://hyunsikwon.github.io/ios/iOS-AVCaptureSession/
'Archive > 패캠(올인원)' 카테고리의 다른 글
ch19 🤖 CreateML 사용 및 코드리뷰 (0) | 2021.07.19 |
---|---|
ch19 🤖 CoreML (0) | 2021.07.19 |
📸 ch 18 AVFoundation 카테고리 별로 탐구 (0) | 2021.07.05 |
ch 18 공짜 계정으로 앱 폰에 설치하기 (0) | 2021.07.05 |
🎬 ch17 Netflix 확장앱 코드리뷰(firebase, kingfisher) + ch15 (0) | 2021.07.05 |