본문 바로가기
🍎 iOS

[Swift] PhotoKit 사용해서 커스텀 앨범 만들기 - UICollectionViewController cell에 PHAsset 넣기

by 틴디 2022. 2. 7.
728x90
반응형

이전 글에서 PhotoKit을 사용할 때 권한을 얻어오고 사용자가 사용하기 쉽도록 대응해보았는데요 PHPhotoLibraryChnageObserver를 먼저 포스팅 해보고 싶었는데 아무래도 UICollectionViewController로 커스텀 앨범의 틀을 만든 후 진행하는 것이 좋을 것 같아 PHAsset에 대한 글 부터 적게 되었어욤

PHAsset 이란?

사용자가 사용하는 모바일 디바이스는 랩탑이나 컴퓨터에 비해 굉장히 한정적인 메모리를 가지고 있습니다 (요즘은 성능이 옛날에 비해 매우매우 좋아졌지만)
그러다 보니 이 메모리를 효율적으로 사용하는 것이 중요합니다. 커스텀 갤러리를 구현하고자 할 때 사용자 라이브러리에 있는 모든 사진을 긁어다가 앱에 저장하고 있다가 사용자가 UITableViewController나 UICollectionViewController에서 보여주면 어떻게 될까요?
텍스트와 비교했을 때 이미지는 텍스트에 비해 고용량 데이터 입니다😔 그러다 보니 한번에 모든 이미지를 가지고 있게 된다면 앱은 터져 버리고 말꺼에요! (처음에 커스텀 갤러리를 어떻게 구현할 지 몰라 PHAsset 전부를 이미지로 바꾸어 앱에 가지고 있었는데 앱이 죽어버리더라구요 ㅠㅠ)

그래서 apple은 사용자 라이브러리에 있는 이미지를 제공할 때 이미지가 아닌 PHAsset이라는 정보를 제공합니다! PHObject의 하위 클래스인데 PHObject는 PHPhotoLibraryChnageObserver에서 살펴보겠습니다~

PHAsset은 사용자 라이브러리의 이미지, 비디오, 라이브 포토의 메타정보만 가지고 있습니다. 이 PHAsset 객체를 그대로 사용하는 것이 아니라 이 PHAsset을 통해서 사진, 라이브 포토, 비디오 등의 asset을 회수해[가져] 오기 위해 Fetching Assets 이라는 과정을 거치게 됩니다. 이 때 사용하는 메소드가 fetchAssets(in:options:)입니다. options는 다양한 반환 옵션을 설정할 수 있는데요, 여기에 에셋 생성 날짜 등의 정렬 옵션도 포함됩니다. 이 메소드의 반환 값은 PHFetchResult<PHAsset> 입니다. 이 불변 array는 모든 asset 컨텐츠를 가지고 있는데요, 여기에 영구적이고 불변한 고유 값 localIdentifier라는 값이 있습니다! PHImageManagerPHCachingImageManager 를 이용해서 이 고유 값을 이미지로 바꿔 줄 수 있습니다!

글로 적혀서 이해가 어려 울 수 있지만 직접 해보시면 바로 이해가 가실 거에요!


1. UI 구성하기

이전 튜토리얼에 이어서 UI를 구성해 주도록 하겠습니다. 이전 글을 보고 오시면 HomeViewController 까지 생성되어 있으실 텐데요, 이제 GallaryViewController를 하나더 생성해 줍시다!

New File 눌러 선택해 주시고 Class는 GallaryViewController로, Subclass of는 UIViewController로 설정해 주시면 됩니다. Also Create XIB file을 체크하셔서 XIB 파일을 함께 생성해 주세요.

원하시는 위치에 Collection View와 닫기 버튼을 위치 시켜 주신 뒤 CollectionView는 IBOutlet을, button은 IBAction을 연결해 주세요. 파일을 하나 더 생성해 줍니다

PhotoCollectionViewCell을 생성해 줍니다. 이 셀이 사진 하나를 담는 UICollectionViewCell 입니다.

PhotoCollectionViewCell에 UIImageView를 추가하고 IBOutlet을 연결해 줍니다.

Attributes Inspector에서 Identifier을 지정해 주시고 엔터 한번 눌러주세요

2. Collection view 구성하기

GallaryViewController에서 두 개의 프로퍼티를 생성해 줍니다

 private let cellWidth = (UIScreen.main.bounds.width - 20) / 3 
 private let kPhotoCell = "PhotoCollectionViewCell"

cell의 사이 마진을 10으로 지정하고 한 행에 세개의 셀을 배치하기 때문에 cellWidth를 이렇게 계산해 주었습니다. cell의 identifier을 상수로 선언하여 사용해주겠습니다.

 override func viewDidLoad() { 
    super.viewDidLoad() 
    let layout = UICollectionViewFlowLayout() 
    layout.minimumLineSpacing = 10 
    layout.minimumInteritemSpacing = 10 
    layout.itemSize = CGSize(width: cellWidth, height: cellWidth) 
    layout.scrollDirection = .vertical 
    
    collectionView.collectionViewLayout = layout 
    collectionView.delegate = self 
    collectionView.dataSource = self 
    collectionView.register(UINib(nibName: kPhotoCell, bundle: nil), forCellWithReuseIdentifier: kPhotoCell) 
}

viewDidLoad에서 collectionView의 기본 설정을 해줍니다. 꼭 cell을 register 해줍니다
다음으로는 UICollectionViewDelegate와 UICollectionViewDataSource 프로토콜을 채택해 줍니다

extension GallaryViewController: UICollectionViewDelegate, UICollectionViewDataSource {
	func numberOfSections(in collectionView: UICollectionView) -> Int { 
    	return 1 
    } 
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return 100 } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 
    	let cell = collectionView.dequeueReusableCell(withReuseIdentifier: kPhotoCell, for: indexPath)
        return cell
    } 
        
}

임의로 필요한 델리게이트와 data source를 구현해 줍시다. 위에서 numberOfSections 를 적지 않아도 문제가 발생하지 않습니다(생략가능)
cell도 dequeue를 사용할 수 있게 해주면 GallaryViewController의 기본 구현은 끝이 났습니다. 이제 HomeViewController로 돌아가서

    func presentGallaryViewController() {
        DispatchQueue.main.async {
            let gallaryViewController = GallaryViewController()
            gallaryViewController.modalPresentationStyle = .overFullScreen
            self.present(gallaryViewController, animated: true, completion: nil)
        }
    }

GallaryViewController을 present 하는 함수를 작성해 줍니다.

 case .authorized, .limited: self.presentGallaryViewController()

커스텀 갤러리인 GallaryViewController을 띄워야 하는 위치에 함수를 호출합니다. 전체적으로 보면 다음과 같습니다

       if #available(iOS 14, *) {
            switch PHPhotoLibrary.authorizationStatus(for: .readWrite) {
            case .notDetermined:
                print("not determined")
                PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in
                    switch status {
                    case .authorized, .limited:
                        self.presentGallaryViewController()
                    case .denied:
                            self.moveToSetting()
                    default:
                        print("그 밖의 권한이 부여 되었습니다.")
                    }
                }
            case .restricted:
                print("restricted")
            case .denied:
                    self.moveToSetting()
            case .limited, .authorized:
                self.presentGallaryViewController()
            default:
                print("")
            }
            
        }

클로저 안에서 UI 작업이 필요하기 때문에 presentGallaryViewController()에서 main 스레드에서 present 되도록 해주어야 합니다.

3. UICollectionViewCell에 Asset 이미지 보여주기

GallaryViewController에서

import Photos

PhotoKit을 사용할 수 있도록 Photos를 임포트 해줍니다.

// 1 
    var asset: PHFetchResult<PHAsset> 

    init() { 
    	// 2 
        let phFetchOptions = PHFetchOptions() 
        phFetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] 
        
        // 3 self.asset = PHAsset.fetchAssets(with: phFetchOptions) 
        super.init(nibName: nil, bundle: nil) 
    } 
    
    required init?(coder: NSCoder) { 
        fatalError("init(coder:) has not been implemented")
    }

1. PHFetchResult<PHAsset> 을 위한 프로퍼티를 선언해 줍니다. 초기값은 옵션을 넣기 위해 init에 작성해 줍니다. lazy var을 사용해도 되긴 하지만 lazy를 사용하면 UI에서 사용하기 전까지 생성이 안되므로 PHPhotoLibraryChnageObserver에서 사용자 라이브러리의 변화를 파악하지 못하고 nil을 반환하게 됩니다.
2. PHFetchOptions로 사진이 생성된 날짜에 따라 오름차순, 내림차순을 정렬해 줄 수 있습니다. 위 코드에서는 "creationDate", ascending은 false로 지정하여 생성 날짜 내림차순으로 정렬된 assets를 반환 받을 수 있습니다.
3. PHFetchResult<PHAsset>을 생성합니다. self.asset으로 사용자 라이브러리에 있는 asset의 정보를 얻을 수 있게 되었습니다.

class PhotoCollectionViewCell: UICollectionViewCell {

    @IBOutlet weak var imageView: UIImageView!
    var representedAssetIdentifier: String?
    
    override func awakeFromNib() {
        super.awakeFromNib()   
    }
}

PhotoCollectionView.siwft에서 representedAssetIdentifier을 선언해 줍니다.
다시 GallaryViewController.swift로 돌아옵니다

let imageManager = PHCachingImageManager()

를 생성해 줍니다. PHImageManager와 PHCachingImageManager가 있는데 PHCachingImageManager의 경우 미리 이미지를 캐싱할 수 있습니다

 func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 
     return asset.count 
 }

셀의 갯수는 asset의 갯수 만큼 설정해 주면 됩니당

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        // 1
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: kPhotoCell, for: indexPath) as! PhotoCollectionViewCell
        // 2
        let asset = self.asset[indexPath.item]
        // 3
        cell.representedAssetIdentifier = asset.localIdentifier
        // 4
        imageManager.requestImage(for: asset, targetSize: CGSize(width: cellWidth, height: cellWidth), contentMode: .aspectFill, options: nil) { image, _ in
            // 5
            if cell.representedAssetIdentifier == asset.localIdentifier {
                // 6
                cell.imageView.image = image
            }
        }
        
        return cell
    }

1. PhotoCollectionViewCell 의 representedAssetIdentifier을 사용할 수 있도록 as! PhotoCollectionViewCell로 강제 타입 변환 해 줍니다.
2. 해당 인덱스에 있는 asset을 asset이라는 상수에 담습니다.
3. ImageManager를 공유해서 사용하기 때문에 5번에서 에셋의 localIdentifier와 비교하기 위해 cell의 representedAssetIdentifier에 asset.localIdentifier을 담아줍니다. localIdentifier는 고유한 값으로 이 값을 사용해 줍니다.
4. imageManager의 requestImage로 asset을 image로 바꾸어 줍니다
5. cell의 localIdentifier와 asset의 localIdentifier가 같은 경우에만 이미지를 변경해 줍니다. reusable셀이고 imageManager을 공유해서 사용하기 때문에 같은 경우에만 이미지를 설정해 줍니다
6. 셀의 이미지 뷰에 이미지를 설정해 줍니다.

앱을 삭제하고 다시 빌드하여 설치해 준뒤 실행해 줍니다. 접근을 허용하고 GallaryViewController를 띄우면 성공적으로 이미지를 바인딩 한걸 확인할 수 있습니다 🥳🥳

requestImage에서 contentMode를 .aspectFill로 해주어도 xib의 설정을 따라 가나 봅니다

PhotoCollectionViewCell.xib로 가셔서 content Mode를 Aspect Fill로 바꾸어 줍니다

이렇게 라이브러리처럼 뜨는 걸 확인 할 수 있어요~~~🤗🤗

다음 튜토리얼에서는 PHPhotoLibraryChnageObserver를 다뤄보도록 하겠습니당
틀리거나 이상한거, 궁금한 건 언제든지 댓글 달아주세요!!🙌🏻


참고 도서 및 사이트
https://developer.apple.com/documentation/photokit/browsing_and_modifying_photo_albums

728x90
반응형

댓글