지난번 iOS의 사진 라이브러리에서 이미지를 가져오는 방법 중 UIImagePickerController를 사용하는 방법에 대해서 알아봤었습니다.
[Swift] 앨범의 이미지를 가져오는 방법(UIImagePickerController)
우선, UIImagePickerController에서 PHPickerViewController로 변경한 이유는
1. 다중 선택 지원
2. 사진의 메타정보 수집(촬영위치, 촬영시간)
두 가지가 가장 필요하기 때문이었습니다.
이 외에도
1. 느린 이미지 로딩과 복구 UI개선
2. Raw와 파노라마 이미지의 안정적인 처리 개선
3. 라이브러리 사용권한 요청없이 PHLivePhoto 사용 가능
4. 유효하지 않는 입력에는 엄격한 규제 추가
등의 이점을 가지고 있기도 합니다.
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
는 PHPickerViewController
의 구성과 표시를 관리합니다. 사용자가 이미지를 선택하면, 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
의 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를 활용해 이미지를 CollectionView로 보여주는 것에 대한 내용이었고, 다음으로는 이미지의 메타데이터를 가져오는 방법입니다.
아이폰의 사진에서 ⓘ를 눌러보면 아래와 같은 정보들을 볼 수 있는데
저는 여기서 '촬영시간'과 '촬영위치'를 불러오도록 하겠습니다.
먼저, 이미지와 그에 대한 메타데이터를 함께 저장할 수 있는 구조체를 정의합니다. 이 구조체는 이미지(UIImage)와 메타데이터(예: 촬영 위치, 촬영 날짜)를 포함합니다.
struct ImageMetadata {
var image: UIImage
var creationDate: Date?
var location: CLLocation?
}
PHPickerViewControllerDelegate
의 picker(_:didFinishPicking:)
메서드 내에서 선택된 이미지의 메타데이터를 추출하고, ImageMetadata
구조체 배열에 저장하는 로직을 추가합니다. 여기서는 PHPickerResult
의 itemProvider
를 사용하여 이미지를 로드하고, 동시에 메타데이터도 함께 추출합니다.
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)
}
}
}
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
}
}
위에서는 위치정보와 촬영시간을 어떻게 처리할지에 대해서는 작성하지 않았습니다. 저는 일기 작성시간과 일기 작성 위치를 사진의 촬영시간과 장소로 대체하는 방법으로 구현하였습니다.
이번에는 PHPickerViewController
를 호출했을 때, 이전에 불러온 이미지를 기억(preselected)하도록 만들어보겠습니다.
먼저, 이미지와 메타데이터를 저장하는 구조체 ImageMetadata
에 선택된 이미지의 assetIdentifier
를 추가합니다. 이 식별자는 PHPickerResult
에서 제공되며, 각 이미지를 고유하게 식별하는 데 사용됩니다.
struct ImageMetadata {
var image: UIImage
var creationDate: Date?
var location: CLLocation?
var identifier: String? // 선택된 이미지의 assetIdentifier 추가
}
ImagePickerManager
의 picker(_:didFinishPicking:)
메서드에서, 각 선택된 이미지의 assetIdentifier
를 ImageMetadata
구조체에 저장합니다. 이 식별자는 나중에 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)
}
}
}
사용자가 photoButton
을 탭했을 때, ImagePickerManager
는 [ImageMetadata]
배열에서 assetIdentifier
들을 추출하여 PHPickerViewController
의 preselectedAssetIdentifiers
에 설정합니다. 이를 통해 사용자가 이전에 선택했던 이미지들이 PHPickerViewController
에서 미리 선택된 상태로 표시됩니다.
class WriteDiaryVC: UIViewController {
@objc func photoButtonTapped() {
// assetIdentifier들을 추출합니다.
let preselectedIdentifiers = imageMetadatas.compactMap { $0.identifier }
imagePickerManager.presentImagePicker(from: self, preselectedIdentifiers: preselectedIdentifiers)
}
}
사용자가 photoButton
을 탭하여 PHPickerViewController
를 다시 호출할 때, 이전에 선택했던 이미지들이 미리 선택된 상태로 표시되도록 preselectedAssetIdentifiers
를 설정합니다. 이를 위해 ImagePickerManager
내에서 createPickerConfiguration
메서드를 수정하여, 이전에 저장된 assetIdentifier
들을 preselectedAssetIdentifiers
로 설정합니다.
만약, configuration.selection
을 ordered
로 설정했다면, 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)
}
}