SwiftUI - 카메라 사용하는 방법 2
사진 캡쳐하고 사진 저장하기
Section 1. 셔터 버튼에 응답하기
private func buttonsView() -> some View {
HStack(spacing: 60) {
Spacer()
NavigationLink {
PhotoCollectionView(photoCollection: model.photoCollection)
.onAppear {
model.camera.isPreviewPaused = true
}
.onDisappear {
model.camera.isPreviewPaused = false
}
} label: {
Label {
Text("Gallery")
} icon: {
ThumbnailView(image: model.thumbnailImage)
}
}
Button {
model.camera.takePhoto()
} label: {
Label {
Text("Take Photo")
} icon: {
ZStack {
Circle()
.strokeBorder(.white, lineWidth: 3)
.frame(width: 62, height: 62)
Circle()
.fill(.white)
.frame(width: 50, height: 50)
}
}
}
Button {
model.camera.switchCaptureDevice()
} label: {
Label("Switch Camera", systemImage: "arrow.triangle.2.circlepath")
.font(.system(size: 36, weight: .bold))
.foregroundColor(.white)
}
Spacer()
}
.buttonStyle(.plain)
.labelStyle(.iconOnly)
.padding()
}
Step 1
takePhoto( )를 시작하면서 시작된다.
버튼이 작동하고, 모델의 카메라 객체에 있는 takePhoto() 메소드를 호출합니다.
Button {
model.camera.takePhoto() // 사진찍기 버튼
}
Section 2. 사진 찍기(캡쳐하기): 사진을 찍을때 카메라는 image data를 센서로 부터 받는다.
func takePhoto() {
guard let photoOutput = self.photoOutput else { return }
sessionQueue.async {
var photoSettings = AVCapturePhotoSettings()
if photoOutput.availablePhotoCodecTypes.contains(.hevc) {
photoSettings = AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.hevc])
}
let isFlashAvailable = self.deviceInput?.device.isFlashAvailable ?? false
photoSettings.flashMode = isFlashAvailable ? .auto : .off
photoSettings.isHighResolutionPhotoEnabled = true
if let previewPhotoPixelFormatType = photoSettings.availablePreviewPhotoPixelFormatTypes.first {
photoSettings.previewPhotoFormat = [kCVPixelBufferPixelFormatTypeKey as String: previewPhotoPixelFormatType]
}
photoSettings.photoQualityPrioritization = .balanced
if let photoOutputVideoConnection = photoOutput.connection(with: .video) {
if photoOutputVideoConnection.isVideoOrientationSupported,
let videoOrientation = self.videoOrientationFor(self.deviceOrientation) {
photoOutputVideoConnection.videoOrientation = videoOrientation
}
}
photoOutput.capturePhoto(with: photoSettings, delegate: self)
}
}
Step 1
photoOutput.capturePhoto(with: photoSettings, delegate: self)
사진을 찍을 때, 사용자들은 가능한 가장 높은 해상도의 이미지를 캡처하고 싶어한다.
이것은 미리보기처럼 빠르게 보기 위한 해상도가 낮은 경향이 있는 미리보기 이미지와 대조된다..
카메라에는 takePhoto() 메서드가 뷰파인더에서 볼 수 있는 고해상도 이미지를 캡처하는 데 사용하는 특수 사진 출력이 있습니다.
Step 2
photoOutput.capturePhoto(with: photoSettings, delegate: self)
사진을 찍기 위해 사진 출력을 요청하여 사진을 찍는 실제 작업을 시작합니다.
모든 것이 잘 진행된다면, 셔터 소리를 들을 수 있다.
Step 3
photoOutput.capturePhoto(with: photoSettings, delegate: self)
capturePhoto를 통해서 사진 출력을 요청하였지만 실제로 찍은 사진을 반환받지 않는다는 것을 확인할 수 있다.
이것은 사진을 캡쳐하는데 시간이 걸리기 때문이다. (초점 맞추고, 플래시 터지는거 기다리고 노출 시간 등을 고려해야함)
capturePhoto는 asynchronous 메서드다, 찍은 사진은 셔터 버튼을 눌른 후 조금 뒤에 도착한다.
Step 4
extension Camera: AVCapturePhotoCaptureDelegate {
func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
if let error = error {
logger.error("Error capturing photo: \(error.localizedDescription)")
return
}
addToPhotoStream?(photo)
}
}
사진 캡쳐가 끝났다면 camera object에 있는 photoOutput(_:didFinishProcessingPhoto:error:) 메서드를 콜백 함수로 받을 것이다.
첫 번째 인수는 캡처된 사진을 AVCapturePhoto의 인스턴스로 받습니다.
AVCapturePhoto = 카메라 캡쳐 출력으로 받은 image data가 들어있는 컨테이너
Step 5
addToPhotoStream?(photo)
이제 캡처한 사진을 얻었으니, 카메라의 photo stream에 추가한다. 그런 다음 data model과 같이 사진을 기다리는 앱의 모든 객체에서 사용할 수 있다.
Section 3. 사진 가공하고 저장하기: 캡쳐된 사진을 어떻게 unpack하고 앨범에 저장하는지 알아보자
final class DataModel: ObservableObject {
let camera = Camera()
let photoCollection = PhotoCollection(smartAlbum: .smartAlbumUserLibrary)
@Published var viewfinderImage: Image?
@Published var thumbnailImage: Image?
var isPhotosLoaded = false
init() {
Task {
await handleCameraPreviews()
}
Task {
await handleCameraPhotos()
}
}
func handleCameraPreviews() async {
let imageStream = camera.previewStream
.map { $0.image }
for await image in imageStream {
Task { @MainActor in
viewfinderImage = image
}
}
}
Step 1:
Task {
await handleCameraPhotos()
}
data model은 새로 캡쳐된 사진을 기다린다. 미리보기 이미지와 마찬가지로, handleCameraPhotos 방법을 사용하여 카메라에서 캡처한 사진 stream을 처리하는 전용 task가 있습니다.
Step 2
func handleCameraPhotos() async {
let unpackedPhotoStream = camera.photoStream
.compactMap { self.unpackPhoto($0) }
for await photoData in unpackedPhotoStream {
Task { @MainActor in
thumbnailImage = photoData.thumbnailImage
}
savePhoto(imageData: photoData.imageData)
}
}
private func unpackPhoto(_ photo: AVCapturePhoto) -> PhotoData? {
guard let imageData = photo.fileDataRepresentation() else { return nil }
guard let previewCGImage = photo.previewCGImageRepresentation(),
let metadataOrientation = photo.metadata[String(kCGImagePropertyOrientation)] as? UInt32,
let cgImageOrientation = CGImagePropertyOrientation(rawValue: metadataOrientation) else { return nil }
let imageOrientation = Image.Orientation(cgImageOrientation)
let thumbnailImage = Image(decorative: previewCGImage, scale: 1, orientation: imageOrientation)
let photoDimensions = photo.resolvedSettings.photoDimensions
let imageSize = (width: Int(photoDimensions.width), height: Int(photoDimensions.height))
let previewDimensions = photo.resolvedSettings.previewDimensions
let thumbnailSize = (width: Int(previewDimensions.width), height: Int(previewDimensions.height))
return PhotoData(thumbnailImage: thumbnailImage, thumbnailSize: thumbnailSize, imageData: imageData, imageSize: imageSize)
}
func savePhoto(imageData: Data) {
Task {
do {
try await photoCollection.addImage(imageData)
logger.debug("Added image data to photo collection.")
} catch let error {
logger.error("Failed to add image to photo collection: \(error.localizedDescription)")
}
}
}
func handleCameraPhotos() async {
let unpackedPhotoStream = camera.photoStream
.compactMap { self.unpackPhoto($0) }
for await photoData in unpackedPhotoStream {
Task { @MainActor in
thumbnailImage = photoData.thumbnailImage // photoData의 썸네일 이미지 저장하기
}
savePhoto(imageData: photoData.imageData) // photoData에 들어있는 이미지데이터 저장하기
}
}
카메라의 photoStream의 각 AVCapturePhoto(사진을 저장하기 위한 컨테이터) 요소는 다른 해상도의 여러 이미지와 이미지의 크기와 이미지가 캡처된 날짜와 시간과 같은 이미지에 대한 다른 메타데이터를 포함할 수 있습니다.
그렇기 때문에 찍은 사진에서 원하는 이미지와 메타데이터를 얻으려면 unpack해야 한다.
handleCameraPhotos 에서 가장 먼저 하는 일은 photoStream을 더 유용한 unpackedPhotoStream으로 변환하는 것이다.
unpackedPhotoStream 안에 담겨있는 여러 각 요소들을 for문으로 풀어서 각각의 변수에 값을 저장한다.
unpack된 각 요소는 원하는 데이터를 포함하는 PhotoData 구조의 인스턴스입니다.
Step 3 : 위에서 self.unpackPhoto는 함수다.
private func unpackPhoto(_ photo: AVCapturePhoto) -> PhotoData? {
guard let imageData = photo.fileDataRepresentation() else { return nil }
guard let previewCGImage = photo.previewCGImageRepresentation(),
let metadataOrientation = photo.metadata[String(kCGImagePropertyOrientation)] as? UInt32,
let cgImageOrientation = CGImagePropertyOrientation(rawValue: metadataOrientation) else { return nil }
let imageOrientation = Image.Orientation(cgImageOrientation)
let thumbnailImage = Image(decorative: previewCGImage, scale: 1, orientation: imageOrientation)
let photoDimensions = photo.resolvedSettings.photoDimensions
let imageSize = (width: Int(photoDimensions.width), height: Int(photoDimensions.height))
let previewDimensions = photo.resolvedSettings.previewDimensions
let thumbnailSize = (width: Int(previewDimensions.width), height: Int(previewDimensions.height))
return PhotoData(thumbnailImage: thumbnailImage, thumbnailSize: thumbnailSize, imageData: imageData, imageSize: imageSize)
}
photoStream을 unpack하기 위해서 unpackPhoto(_:) 함수를 사용한다.
이 함수는 photoStream를 unpack하면 저해상도의 썸네일를 (Image 타입) + 높은 해상도의 이미지(Data 타입) +높은 해상도 사진의 크기를 PhotoData 인스턴스로 반환받아서 사용할 수 있다.
Step 4
let unpackedPhotoStream = camera.photoStream
.compactMap { self.unpackPhoto($0) }
비동기 stream으로서 photoStream은 시퀀스(순차적이고 반복적인)와 매우 유사하다.
compactMap(_:) 메소드를 사용하여 스트림의 각 사진 ($0)에 대해 unpackPhoto(_:)를 호출할 수 있습니다.
이것은 AVCapturePhoto 인스턴스의 스트림을 PhotoData 인스턴스의 훨씬 더 유용한 스트림으로 변환합니다.
Step 5
func handleCameraPhotos() async {
let unpackedPhotoStream = camera.photoStream
.compactMap { self.unpackPhoto($0) }
for await photoData in unpackedPhotoStream {
Task { @MainActor in
thumbnailImage = photoData.thumbnailImage
}
savePhoto(imageData: photoData.imageData)
}
}
For-await 루프는 이제 처리하기 전에 photoData 에 unpackedPhotoStream (PhotoData타입) 가 들어갈때까지 기다린다.
unpackedPhotoStream : unpackPhoto함수를 사용하여 저해상도의 썸네일를 (Image 타입) + 높은 해상도의 이미지(Data 타입) +높은 해상도 사진의 크기 등을 반환 받아 PhotoData 타입을 담고 있다.
Step 6
thumbnailImage = photoData.thumbnailImage
photoData에 썸네일 이미지(Image타입)를 model의 thumbnailImage 프로퍼티를 업데이트하는데 사용한다.
Step 7
savePhoto(imageData: photoData.imageData)
모델의 savePhoto(imageData:) 메소드를 호출하여 photoData(Data 타입)의 이미지 데이터를 사진 라이브러리의 새 사진으로 저장
Step 8
func savePhoto(imageData: Data) {
Task {
do {
try await photoCollection.addImage(imageData)
logger.debug("Added image data to photo collection.")
} catch let error {
logger.error("Failed to add image to photo collection: \(error.localizedDescription)")
}
}
}
savePhoto(imageData:) 메서드는 Task 내부에서 addImage(_:) 메서드를 호출하여 사진 데이터를 photoCollection 객체에 저장하는 실제 작업을 전달합니다. 데이터 모델의 임무는 앱의 데이터 객체 간의 데이터 흐름을 조정하는 것이다.