이전에 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"
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()
}
}
복잡한 부분들이 있으니 하나씩 자세히 알아보겠다.
let cameraRoll = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumUserLibrary, options: nil)
PHAssetCollection.fetchAssetCollections메서드는 특정 조건에 맞는 앨범을 반환한다.
반환 타입은 PHFetchResult타입이다.
.smartAlbum은 기본 앨범 타입을 나타낸다.
.smartAlbumUserLibrary는 사용자의 사진첩 전체를 나타내는 서브타입이다.
option은 nil로 설정되어, 특별한 옵션 없이 앨범을 가져온다.
즉 특별한 옵션 없이 기본 앨범의 전체 이미지를 포함한 기본 앨범을 저장한다.
'카메라 롤'? 이란 용어가 생소해서 찾아보았다. 우리가 흔히 사진이나 영상을 촬영하고 이미지를 다운받았을 때 기본적으로 저장되는 앨범을 카메라 롤이라고 한다고 한다.
guard let cameraRollCollection = cameraRoll.firstObject else {
return
}
해당 코드는 camerRoll에 있는 첫 번째 객체(카메라 롤)를 반환한다.
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
self.fetchResult = PHAsset.fetchAssets(in: cameraRollCollection, options: fetchOptions)
PHFetchOptions를 사용하여 앨범에서 사진을 가져올 때 옵션을 설정한다.
sortDescriptors는 사진 정렬 기준을 결정한다. ("creationDate"은 사진이 생성된 날짜, 오름차순)
fetchAssets를 사용하여 위에 설정한 옵션을 기준으로 사진을 가져오고 위에서 선언한 변수인 fetchResult에 저장한다.
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)
// ...
}
}
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
}
기본 테이블뷰 관련된 내용은 생략하고 정리해본다.
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)
}
}
이렇게 PhotoKit을 사용하여 기본적인 이미지 관리 기능 구현 코드를 살펴보았다. 확실히 이전보다 이해도가 높아졌다. 그래도 어렵네~
큰 도움이 되었습니다, 감사합니다.