PhotoKit

iCloud 사진, 라이브 포토 등을 포함한 사진 앱에서 관리하는 모든 데이터를 사용하여 작업할 수 있는 프레임워크. 에셋을 가져오고, 캐시, 편집 등을 포함한 기능을 지원한다.

권한

갤러리에 접근하기 위해서는 권한이 필요하다. 이번 앱에서 사용할 권한은 ‘Privacy - Photo Library Additions Usage Description’으로 사진을 저장할 수 있는 권한만을 가져오는 것이다.

수정: 사진을 저장하는 것에는 위 권한으로 충분했지만 앨범에 묶어 저장하는 기능을 추가하는 과정에서 ‘Privacy - Photo Library Usage Description’ 권한이 필요해졌다. 앨범이 있는지 확인하는 과정에서 읽기 권한이 필요하기 때문이다.

수정2: 권한에 추가적인 문제점이 발견되어 하단에 추가로 정리함. 글을 가능하면 변경 흐름에 따라 정리하기 위해 이곳에는 변경 여부만 기록.

코드 예시

import Photos
import UIKit

struct PhotoSaver {
    func saveImage(image: UIImage, completion: @escaping (Bool, Error?) -> Void) {
        // 권한 확인
        PHPhotoLibrary.requestAuthorization(for: .addOnly) { status in
            guard status == .authorized || status == .limited else {
                completion(false, nil)
                return
            }
            // 라이브러리 저장
            PHPhotoLibrary.shared().performChanges {
                PHAssetChangeRequest.creationRequestForAsset(from: image)
            } completionHandler: { result, error in
                DispatchQueue.main.async {
                    completion(result, error)
                }
            }
        }
    }
}

PHPhotoLibrary를 통해 권한을 확인하고, creationRequestForAsset을 통해 이미지 생성을 요청하고, 그 결과를 performChanges 하는 정형적인 로직을 통해 결과를 저장할 수 있다.

앨범(폴더) 저장

갤러리에 앨범(폴더)을 만들어 해당 앨범 내에 앱에서 생성한 모든 결과물을 묶어 저장하게 할 수 있다.

PHAssetColletion.fetchAssetCollection을 이용해 앨범이 있는지 확인하고, 없으면 새로 만든다.

저장하는 방식이 조금 특이했다.

PHPhotoLibrary.shared().performChanges {
    let assetRequest = PHAssetChangeRequest.creationRequestForAsset(from: image)

    if let album = album,
       let placeholder = assetRequest.placeholderForCreatedAsset,
       let albumRequest = PHAssetCollectionChangeRequest(for: album) {
        albumRequest.addAssets([placeholder] as NSArray)
    }
} completionHandler: { success, error in
    DispatchQueue.main.async {
        completion(success, error)
    }
}

위에서는 performChanges에 대해 자세히 다루지 않았었는데 이는 트랜잭션의 역할을 수행한다.

creationRequestForAsset은 사진을 최근 항목에 저장한다. 한편 사진 저장이 시작되지 않았기 때문에 아직 앨범에 들어갈 수 없는데, placeholder를 만들어 앨범에 들어갈 자리를 미리 만들어 두는 방식이다.

문제점

맥(Design for iPad)에서는 권한을 묻는 팝업이 뜨지 않고, requestAuthorization 함수가 .denied를 반환하고 있었다. 이미 권한을 수락하거나 거절한 적이 있어 발생하는 문제를 예상해 보았지만 맥에서 사진 앱의 권한은 본 앱에 대해 깨끗한 상태였고, 캐시를 제거하고 다시 실행해보아도 사용자에게 권한을 묻는 메시지는 등장하지 않았다.

위 문제는 Design For iPad로 Mac에서 실행시킨 것 때문에 발생한 상황으로 예상된다.

한편 AI의 조언으로 ‘Mac 사용자는 사진 앱(갤러리)에 저장하기보다는 파일로 저장하는 상황을 더욱 기대할 것’이라는 말을 들었는데, 실제로 맥에서는 사진을 파일로 다룰 가능성이 더 높다고 보고 Mac인 경우에는 FileExporter로 저장하도록 변경.