PhotoKit 직접 사용해보기

고라니·2023년 8월 10일
0

TIL

목록 보기
22/67

이전에 PhotoKit에 대한 개념을 공부했었다. 그럼에도 불구하고 몇몇 부분이 이해가 잘 가지 않아 실제로 코드로 어떻게 구현할지 알아보았다. 카메라 롤에서 사진을 가져와 테이블 뷰에 보여주고, 그 사진들을 테이블뷰의 edit 모드로 삭제하는 기능을 구현 해보자

완성 코드

import UIKit
import Photos

class ViewController: UIViewController, PHPhotoLibraryChangeObserver {

    @IBOutlet weak var tableView: UITableView!
    var fetchResult: PHFetchResult<PHAsset>!
    let imageManager: PHCachingImageManager = PHCachingImageManager()
    let cellIdentifier: String = "cell"

    override func viewDidLoad() {
        super.viewDidLoad()
        setupDelegate()
        checkPhotoAuthorizationStatus()
        PHPhotoLibrary.shared().register(self)
    }

    func requestCollection() {
        let cameraRoll = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumUserLibrary, options: nil)

        guard let cameraRollCollection = cameraRoll.firstObject else {
            return
        }

        let fetchOptions = PHFetchOptions()
        fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
        self.fetchResult = PHAsset.fetchAssets(in: cameraRollCollection, options: fetchOptions)

        DispatchQueue.main.async {
            self.tableView.reloadData()
        }
    }

    func requestPhotoLibraryPermission() {
        let status = PHPhotoLibrary.authorizationStatus()

        switch status {
        case .notDetermined:
            PHPhotoLibrary.requestAuthorization { (newStatus) in
                if newStatus == .authorized {
                    // 권한 허가됨
                    self.requestCollection()

                } else {
                    // 권한 거부됨
                }
            }
        case .restricted, .denied:
            // 권한 거부됨
            break
        case .authorized:
            // 이미 권한 허가됨
            self.requestCollection()
        case .limited:
            // iOS 14+에서 제한된 접근 권한 상태
            break
        @unknown default:
            // 알 수 없는 새로운 상태 (미래의 iOS 버전에서 추가될 수 있음)
            break
        }
    }

    func setupDelegate() {
        self.tableView.delegate = self
        self.tableView.dataSource = self
    }

    func photoLibraryDidChange(_ changeInstance: PHChange) {
        guard let changes = changeInstance.changeDetails(for: fetchResult) else {
            return
        }

        fetchResult = changes.fetchResultAfterChanges

        DispatchQueue.main.async {
            self.tableView.reloadSections(IndexSet(0...0), with: .automatic)
        }
    }
}

// TableView DataSource & Delegate
extension ViewController: UITableViewDelegate, UITableViewDataSource {
    func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
        return true
    }

    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        if editingStyle == .delete {
            let asset: PHAsset = self.fetchResult[indexPath.row]

            PHPhotoLibrary.shared().performChanges({
                PHAssetChangeRequest.deleteAssets([asset] as NSArray)
            }, completionHandler: nil)
        }
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return fetchResult?.count ?? 0
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath)
        var content = cell.defaultContentConfiguration()

        let asset = fetchResult.object(at: indexPath.row)
        let options = PHImageRequestOptions()
        options.resizeMode = .exact

        imageManager.requestImage(for: asset,
                                  targetSize: CGSize(width: 50, height: 50),
                                  contentMode: .aspectFill,
                                  options: options) { image, _ in
            content.image = image
            cell.contentConfiguration = content
        }
        return cell
    }
}

변수 선언

    var fetchResult: PHFetchResult<PHAsset>!
    let imageManager: PHCachingImageManager = PHCachingImageManager()
    let cellIdentifier: String = "cell"
  • fetchResult: 사진앱에서 가져온 이미지의 목록을 저장한다.
  • imageManager: 이미지를 불러올 때 사용
  • cellIdentifier: 데이블 뷰 재사용 셀을 위한 식별자

PHFetchResult: 제네릭 클래스로 주로 PHAsset, PHAssetCollection, PHCollectionList 등의 객체들의 목록을 반환하는 데 사용된다. 제네릭 타입이기 때문에 꺾쇠괄호 내부에 어떤 타입의 데이터를 저장하고자 하는지 지정해 주어야 한다.

PHCachingImageManager: 이미지 및 비디오 컨텐츠를 빠르게 로딩하고 캐싱하는데 사용된다. 추후에 PHAsset에 연결된 이미지를 요청한다.

권한 확인

func requestPhotoLibraryPermission() {
        let status = PHPhotoLibrary.authorizationStatus()

        switch status {
        case .notDetermined:
        	// 결정되지 않음
            PHPhotoLibrary.requestAuthorization { (newStatus) in
                if newStatus == .authorized {
                    // 권한 허가됨
                    self.requestCollection()
                } else {
                    // 권한 거부됨
                }
            }
        case .restricted, .denied:
            // 권한 거부됨
            break
        case .authorized:
            // 이미 권한 허가됨
            self.requestCollection()
        case .limited:
            // iOS 14+에서 제한된 접근 권한 상태
            break
        @unknown default:
            // 알 수 없는 새로운 상태 (미래의 iOS 버전에서 추가될 수 있음)
            break
        }
    }

먼저 PhotoKit을 이용하여 사용자의 사진첩에 접근하기 위해서 접근 권한이 필요하다.
info.plist에 Privacy - Photo Library Usage Description을 추가한다.

다음으로 사용자 권한을 확인하고 요청하는 requestPhotoLibraryPermission()을 작성해준다.
switch문을 통해 각 권한 상태에 따른 처리를 해주면 된다. 권한이 거부되었을 때의 처리는 알럴트 등을 통해 처리해주면 된다.

authorizationStatus(): 현재 앱의 사진 라이브러리 접근 권한 상태를 반환, 반환 타입은 PHAuthorizationStatus의 열거형

requestAuthorization(_:): 사용자에게 사진 라이브러리 접근 권한을 요청한다. 클로저 파라미터를 제공하여 사용자가 권한 요청을 처리한 후 호출한다.

이미지 가져오기

권한 요청이 완료되면 requestCollection()을 호출하여 사진앱의 이미지를 가져온다.

func requestCollection() {
        let cameraRoll = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumUserLibrary, options: nil)

        guard let cameraRollCollection = cameraRoll.firstObject else {
            return
        }

        let fetchOptions = PHFetchOptions()
        fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
        self.fetchResult = PHAsset.fetchAssets(in: cameraRollCollection, options: fetchOptions)

        DispatchQueue.main.async {
            self.tableView.reloadData()
        }
    }

복잡한 부분들이 있으니 하나씩 자세히 알아보겠다.

1

let cameraRoll = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumUserLibrary, options: nil)

PHAssetCollection.fetchAssetCollections메서드는 특정 조건에 맞는 앨범을 반환한다.
반환 타입은 PHFetchResult타입이다.

.smartAlbum은 기본 앨범 타입을 나타낸다.
.smartAlbumUserLibrary는 사용자의 사진첩 전체를 나타내는 서브타입이다.
option은 nil로 설정되어, 특별한 옵션 없이 앨범을 가져온다.

즉 특별한 옵션 없이 기본 앨범의 전체 이미지를 포함한 기본 앨범을 저장한다.

'카메라 롤'? 이란 용어가 생소해서 찾아보았다. 우리가 흔히 사진이나 영상을 촬영하고 이미지를 다운받았을 때 기본적으로 저장되는 앨범을 카메라 롤이라고 한다고 한다.

2

guard let cameraRollCollection = cameraRoll.firstObject else {
    return
}

해당 코드는 camerRoll에 있는 첫 번째 객체(카메라 롤)를 반환한다.

3

let fetchOptions = PHFetchOptions()
        fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
        self.fetchResult = PHAsset.fetchAssets(in: cameraRollCollection, options: fetchOptions)

PHFetchOptions를 사용하여 앨범에서 사진을 가져올 때 옵션을 설정한다.
sortDescriptors는 사진 정렬 기준을 결정한다. ("creationDate"은 사진이 생성된 날짜, 오름차순)
fetchAssets를 사용하여 위에 설정한 옵션을 기준으로 사진을 가져오고 위에서 선언한 변수인 fetchResult에 저장한다.

4

DispatchQueue.main.async {
    self.tableView.reloadData()
}

UI는 항상 메인 스레드에서 작업되어야 하기때문에 DispatchQueue.main.async 블럭 내부에 테이블뷰를 다시 로드시킨다. 관련 내용은 추후에 정리해보겠다.

변화 감지

func photoLibraryDidChange(_ changeInstance: PHChange) {
        guard let changes = changeInstance.changeDetails(for: fetchResult) else {
            return
        }

        fetchResult = changes.fetchResultAfterChanges

        DispatchQueue.main.async {
            self.tableView.reloadSections(IndexSet(0...0), with: .automatic)
        }
    }

이 함수는 PHPhotoLibraryChangeObserver 프로토콜 메서드로 사진첩에 변경이 발생할 때 자동으로 호출된다.

이전에 먼저 프로토콜을 채택하고 해당 뷰컨트롤러를 옵저버로 등록해주어야 한다.

class ViewController: UIViewController, PHPhotoLibraryChangeObserver {
	override func viewDidLoad() {
        // ...
        PHPhotoLibrary.shared().register(self)
        // ...
    }
}
  • changeInstance는 사진첩의 변경 사항을 포함하는 객체다.
  • changeDetails(for:)는 특정 PHFechResult에 대한 변경 사항을 가져온다. fetchResult를 파라미터로 입력하여 fetchResult의 변경사항을 가져온다, 만약 변경사항이 없다면 함수는 종료된다.
  • changes.fetchResultAfterChanges(변경 이후 데이터)를 다시 fetchResult에 할당하여 변경사항을 업데이트 한다.
  • 현재 코드상 섹션이 하나밖에 없기때문에 전체를 리로드하지 않고 섹션 하나만 리로드 해준다.

테이블뷰 구성

DataSource

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return fetchResult?.count ?? 0
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell: UITableViewCell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath)
    var content = cell.defaultContentConfiguration()

    let asset = fetchResult.object(at: indexPath.row)
    let options = PHImageRequestOptions()
    options.resizeMode = .exact

    imageManager.requestImage(for: asset,
                              targetSize: CGSize(width: 50, height: 50),
                              contentMode: .aspectFill,
                              options: options) { image, _ in
    content.image = image
    cell.contentConfiguration = content
    }
     return cell
}

기본 테이블뷰 관련된 내용은 생략하고 정리해본다.

  • asset에 fetchResult.object에 indexPath.row를 이용하여 현재 행에 해당하는 사진 에셋을 가져온다.
  • imageManger.requestImage를 사용하여 실제 이미지로 가지고 온다.
  • imageManager.requestImage(for:targetSize:contentMode:options:resultHandler:)로 PHAsset에서 이미지를 가져오는 작업을 수행한다.
  • resultHandler를 통해 가져온 이미지를 셀의 content.image로 설정한다.

Delegate

func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
    return true
}

func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
    if editingStyle == .delete {
        let asset: PHAsset = self.fetchResult[indexPath.row]

        PHPhotoLibrary.shared().performChanges({
            PHAssetChangeRequest.deleteAssets([asset] as NSArray)
        }, completionHandler: nil)
    }
}
  • 첫번째 delegate 메소드는 tableView의 편집기능 사용 여부를 반환한다. (true: 편집기능 사용)
  • 두번째 메소드는 테이블 뷰의 특정 셀이 특정 편집 작업을 수행할 때 호출된다.
  • PHPhotoLibrary의 performChanges 메서드를 사용하여 PHAsset을 삭제하는 작업을 요청한다. 실제로 사용자의 앨범에서 해당하는 사진은 삭제된다.

마치면서

이렇게 PhotoKit을 사용하여 기본적인 이미지 관리 기능 구현 코드를 살펴보았다. 확실히 이전보다 이해도가 높아졌다. 그래도 어렵네~

profile
🍎 무럭무럭

2개의 댓글

comment-user-thumbnail
2023년 8월 10일

큰 도움이 되었습니다, 감사합니다.

1개의 답글