[Swift] 앨범에서 이미지를 메타데이터(촬영시간, 위치정보)와 함께 가져오기(PHPickerViewController)

이경은·2024년 4월 4일
0
post-custom-banner

지난번 iOS의 사진 라이브러리에서 이미지를 가져오는 방법 중 UIImagePickerController를 사용하는 방법에 대해서 알아봤었습니다.
[Swift] 앨범의 이미지를 가져오는 방법(UIImagePickerController)

우선, UIImagePickerController에서 PHPickerViewController로 변경한 이유는
1. 다중 선택 지원
2. 사진의 메타정보 수집(촬영위치, 촬영시간)
두 가지가 가장 필요하기 때문이었습니다.

이 외에도
1. 느린 이미지 로딩과 복구 UI개선
2. Raw와 파노라마 이미지의 안정적인 처리 개선
3. 라이브러리 사용권한 요청없이 PHLivePhoto 사용 가능
4. 유효하지 않는 입력에는 엄격한 규제 추가
등의 이점을 가지고 있기도 합니다.

PHPickerViewController를 사용해 이미지를 불러오는 방법

WriteDiaryVC의 기본 구성

WriteDiaryVC에는 사용자가 이미지를 선택할 수 있는 photoButton과, 선택된 이미지를 표시할 imagesCollectionView가 있습니다. 사용자가 photoButton을 탭하면 PHPickerViewController가 표시되어 이미지를 선택할 수 있고, 선택된 이미지들은 imagesCollectionView에 동적으로 표시됩니다.

import UIKit
import PhotosUI

class WriteDiaryVC: UIViewController {
    var imagePickerManager = ImagePickerManager()
    var selectedImages: [UIImage] = [] // 선택된 이미지들을 저장할 배열
    
    private lazy var photoButton: UIButton = {
        let button = UIButton()
        button.setImage(UIImage(systemName: "photo"), for: .normal)
        button.addTarget(self, action: #selector(photoButtonTapped), for: .touchUpInside)
        return button
    }()
    
    private lazy var imagesCollectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .horizontal
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView.backgroundColor = .white
        collectionView.register(ImageCell.self, forCellWithReuseIdentifier: "ImageCell")
        collectionView.dataSource = self
        collectionView.delegate = self
        return collectionView
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
    }
    
    private func setupUI() {
        // UI 설정 코드
        view.addSubview(photoButton)
        view.addSubview(imagesCollectionView)
    }
    
    @objc func photoButtonTapped() {
        imagePickerManager.presentImagePicker(from: self)
    }
}

ImagePickerManager를 통한 이미지 선택

ImagePickerManagerPHPickerViewController의 구성과 표시를 관리합니다. 사용자가 이미지를 선택하면, ImagePickerManager는 선택된 이미지를 처리하여 WriteDiaryVC로 전달합니다.

import PhotosUI

class ImagePickerManager: NSObject {
    weak var delegate: WriteDiaryVC?
    
    func presentImagePicker(from viewController: WriteDiaryVC) {
        var configuration = PHPickerConfiguration()
        configuration.selectionLimit = 0 // 무제한 선택
        configuration.filter = .images // 이미지만 선택
        
        let picker = PHPickerViewController(configuration: configuration)
        picker.delegate = self
        viewController.present(picker, animated: true, completion: nil)
        self.delegate = viewController
    }
}

extension ImagePickerManager: PHPickerViewControllerDelegate {
    func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
        picker.dismiss(animated: true, completion: nil)
        
        // 선택된 이미지 처리
        for result in results {
            result.itemProvider.loadObject(ofClass: UIImage.self) { (image, error) in
                DispatchQueue.main.async {
                    if let image = image as? UIImage {
                        self.delegate?.selectedImages.append(image)
                        self.delegate?.imagesCollectionView.reloadData()
                    }
                }
            }
        }
    }
}

imagesCollectionView에 이미지 표시

imagesCollectionView의 dataSource와 delegate를 설정하여, 선택된 이미지들을 표시합니다. 사용자가 새로운 이미지를 선택할 때마다 collectionView는 업데이트되어 선택된 이미지들을 표시합니다.

extension WriteDiaryVC: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return selectedImages.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ImageCell", for: indexPath) as? ImageCell else {
            return UICollectionViewCell()
        }
        cell.imageView.image = selectedImages[indexPath.row]
        return cell
    }
    
    // UICollectionViewDelegateFlowLayout 메서드로 셀 크기 등을 설정
}

PHPickerViewController를 사용해 메타정보를 함께 불러오는 방법

여기까지는 PHPickerViewController를 활용해 이미지를 CollectionView로 보여주는 것에 대한 내용이었고, 다음으로는 이미지의 메타데이터를 가져오는 방법입니다.

아이폰의 사진에서 ⓘ를 눌러보면 아래와 같은 정보들을 볼 수 있는데

저는 여기서 '촬영시간'과 '촬영위치'를 불러오도록 하겠습니다.

메타데이터를 포함한 이미지 정보 정의

먼저, 이미지와 그에 대한 메타데이터를 함께 저장할 수 있는 구조체를 정의합니다. 이 구조체는 이미지(UIImage)와 메타데이터(예: 촬영 위치, 촬영 날짜)를 포함합니다.

struct ImageMetadata {
    var image: UIImage
    var creationDate: Date?
    var location: CLLocation?
}

메타데이터 추출 및 저장

PHPickerViewControllerDelegatepicker(_:didFinishPicking:) 메서드 내에서 선택된 이미지의 메타데이터를 추출하고, ImageMetadata 구조체 배열에 저장하는 로직을 추가합니다. 여기서는 PHPickerResultitemProvider를 사용하여 이미지를 로드하고, 동시에 메타데이터도 함께 추출합니다.

extension ImagePickerManager: PHPickerViewControllerDelegate {
    func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
        picker.dismiss(animated: true, completion: nil)
        
        var imageMetadatas: [ImageMetadata] = []
        
        let group = DispatchGroup()
        
        results.forEach { result in
            group.enter()
            result.itemProvider.loadObject(ofClass: UIImage.self) { (object, error) in
                defer { group.leave() }
                
                guard let image = object as? UIImage else { return }
                
                // 기본적인 메타데이터 생성
                var imageMetadata = ImageMetadata(image: image, creationDate: nil, location: nil)
                
                // 메타데이터 추출
                // 예: 촬영 날짜와 위치 정보 추출
                if let assetId = result.assetIdentifier, let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: nil).firstObject {
                    imageMetadata.creationDate = asset.creationDate
                    imageMetadata.location = asset.location
                }
                
                DispatchQueue.main.async {
                    imageMetadatas.append(imageMetadata)
                }
            }
        }
        
        group.notify(queue: .main) {
            self.delegate?.updateCollectionView(with: imageMetadatas)
        }
    }
}

CollectionView 업데이트

WriteDiaryVC 내에서 ImageMetadata 구조체 배열을 사용하여 imagesCollectionView를 업데이트하는 메서드인 updateCollectionView를 추가합니다.

class WriteDiaryVC: UIViewController {
    var imageMetadatas: [ImageMetadata] = []
    
    // 기타 코드...
    
    func updateCollectionView(with imageMetadatas: [ImageMetadata]) {
        self.imageMetadatas = imageMetadatas
        imagesCollectionView.reloadData()
    }
}

extension WriteDiaryVC: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return imageMetadatas.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ImageCell", for: indexPath) as? ImageCell else {
            return UICollectionViewCell()
        }
        
        let imageMetadata = imageMetadatas[indexPath.row]
        cell.imageView.image = imageMetadata.image
        
        // 메타데이터 정보를 셀에 표시하는 코드 추가
        // 예: cell.dateLabel.text = imageMetadata.creationDate?.formattedDate
        // 예: cell.locationLabel.text = imageMetadata.location?.coordinateDescription
        
        return cell
    }
}

위에서는 위치정보와 촬영시간을 어떻게 처리할지에 대해서는 작성하지 않았습니다. 저는 일기 작성시간과 일기 작성 위치를 사진의 촬영시간과 장소로 대체하는 방법으로 구현하였습니다.

AssetIdentifier로 preselected 시키기

이번에는 PHPickerViewController를 호출했을 때, 이전에 불러온 이미지를 기억(preselected)하도록 만들어보겠습니다.

ImageMetadata 구조체 업데이트

먼저, 이미지와 메타데이터를 저장하는 구조체 ImageMetadata에 선택된 이미지의 assetIdentifier를 추가합니다. 이 식별자는 PHPickerResult에서 제공되며, 각 이미지를 고유하게 식별하는 데 사용됩니다.

struct ImageMetadata {
    var image: UIImage
    var creationDate: Date?
    var location: CLLocation?
    var identifier: String?  // 선택된 이미지의 assetIdentifier 추가
}

선택된 이미지의 assetIdentifier 저장

ImagePickerManagerpicker(_:didFinishPicking:) 메서드에서, 각 선택된 이미지의 assetIdentifierImageMetadata 구조체에 저장합니다. 이 식별자는 나중에 PHPickerViewController를 호출할 때 미리 선택된 이미지로 표시하기 위해 사용됩니다.

extension ImagePickerManager: PHPickerViewControllerDelegate {
    func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
        picker.dismiss(animated: true, completion: nil)
        
        var imageMetadatas: [ImageMetadata] = []
        let group = DispatchGroup()
        
        results.forEach { result in
            group.enter()
            result.itemProvider.loadObject(ofClass: UIImage.self) { (object, error) in
                defer { group.leave() }
                
                guard let image = object as? UIImage else { return }
                
                // 메타데이터 초기화
                var imageMetadata = ImageMetadata(image: image, creationDate: nil, location: nil, identifier: result.assetIdentifier)
                
                // Photos Framework를 사용하여 추가 메타데이터 추출
                if let assetId = result.assetIdentifier {
                    let assets = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: nil)
                    if let asset = assets.firstObject {
                        // 촬영 날짜와 위치 정보 추출
                        imageMetadata.creationDate = asset.creationDate
                        imageMetadata.location = asset.location
                    }
                }
                
                DispatchQueue.main.async {
                    imageMetadatas.append(imageMetadata)
                }
            }
        }
        
        // 모든 이미지 처리 완료 후, 메인 스레드에서 collectionView 업데이트
        group.notify(queue: .main) {
            self.delegate?.updateCollectionView(with: imageMetadatas)
        }
    }
}

ImagePickerManager에 preselectedAssetIdentifiers 전달

사용자가 photoButton을 탭했을 때, ImagePickerManager[ImageMetadata] 배열에서 assetIdentifier들을 추출하여 PHPickerViewControllerpreselectedAssetIdentifiers에 설정합니다. 이를 통해 사용자가 이전에 선택했던 이미지들이 PHPickerViewController에서 미리 선택된 상태로 표시됩니다.

class WriteDiaryVC: UIViewController {
    @objc func photoButtonTapped() {
        // assetIdentifier들을 추출합니다.
        let preselectedIdentifiers = imageMetadatas.compactMap { $0.identifier }
        imagePickerManager.presentImagePicker(from: self, preselectedIdentifiers: preselectedIdentifiers)
    }
}

preselectedAssetIdentifiers 설정

사용자가 photoButton을 탭하여 PHPickerViewController를 다시 호출할 때, 이전에 선택했던 이미지들이 미리 선택된 상태로 표시되도록 preselectedAssetIdentifiers를 설정합니다. 이를 위해 ImagePickerManager 내에서 createPickerConfiguration 메서드를 수정하여, 이전에 저장된 assetIdentifier들을 preselectedAssetIdentifiers로 설정합니다.

만약, configuration.selectionordered로 설정했다면, preselectedIdentifiers의 배열 순서대로 1, 2, 3 ... 번호가 매겨진 상태로 선택됩니다.

class ImagePickerManager: NSObject {
    // 이전에 선택된 이미지들의 assetIdentifier를 저장하는 배열
    var preselectedIdentifiers: [String] = []

func presentImagePicker(from viewController: UIViewController, preselectedIdentifiers: [String]) {
        var configuration = PHPickerConfiguration(photoLibrary: .shared())
        configuration.selectionLimit = 0  // 무제한 선택
        configuration.filter = .images  // 이미지만 선택
        configuration.preselectedAssetIdentifiers = preselectedIdentifiers  // 이전에 선택된 이미지 식별자 설정
        
        let picker = PHPickerViewController(configuration: configuration)
        picker.delegate = self
        viewController.present(picker, animated: true)
    }
}
post-custom-banner

0개의 댓글