swiftUI

SwiftUI - 카메라 사용하는 방법 2

Coding_happyytw 2023. 9. 28. 15:52

사진 캡쳐하고 사진 저장하기

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 객체에 저장하는 실제 작업을 전달합니다. 데이터 모델의 임무는 앱의 데이터 객체 간의 데이터 흐름을 조정하는 것이다.