관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] Photos 프레임워크 - 앨범, 사진 가져오기 (PHAsset, PHAssetCollection, PHCollectionList, PHCachingImageManager, PHFetchResult<PHAsset>) 본문

iOS framework

[iOS - swift] Photos 프레임워크 - 앨범, 사진 가져오기 (PHAsset, PHAssetCollection, PHCollectionList, PHCachingImageManager, PHFetchResult<PHAsset>)

jake-kim 2022. 7. 2. 17:01

구현 아이디어

  • PhotoService라는 클래스를 싱글톤으로 만든 후, 이 서비스를 통해서 앨범에 있는 사진을 꺼내오도록 구현
  • PhotoService의 주요 메소드
    • getAlbums(): iOS에는 일반 앨범과 스마트 앨범이 있는데 이 앨범 정보들을 불러오는 메소드
    • getPHAssets(album:): 앨범을 파라미터로 주면 해당 앨범에 있는 이미지들 [PHAsset] 정보를 가져오는 메소드
    • fetchImages(asset:size:contentMode:): asset을 파라미터로 주면, 해당 asset을 UIImage로 변경하는 메소드

구현

  • UI 구성
    • ViewController에서 album버튼을 누르면 PhotoViewController 화면이 나오고, 이 화면은 collectionView와 pickerView로 이루어진 화면

PhotoViewController (collectionView + pickerView)

  • PhotoService에 - 앨범들을 가져오는 메소드 정의
    • iOS에서 내부 앨범을 가져오는 작업은 비동기 작업이므로 completion에서 값을 넘겨주도록 구현
    • PHAsset: iOS의 Photos 프레임워크에 있는 형태이며, 이미지와 동영상의 또 다른 타입이라고 기억
    • PHFetchResult<PHAsset>: PHAsset 정보들을 담고 있는 `앨범`이라고 기억
func getAlbums(mediaType: MediaType, completion: @escaping ([AlbumInfo]) -> Void)

// 미디어 타입 구분
enum MediaType {
  case all
  case image
  case video
}

// 앨범 정보가 들어갈 모델
struct AlbumInfo: Identifiable {
  let id: String?
  let name: String
  let count: Int
  let album: PHFetchResult<PHAsset>
}
  • (getAlbums 메소드 구현)
    • 앨범 배열을 선언해놓고, 일반 앨범과 스마트 앨범을 각각 조회해서 배열에다 추가해놓게 구현
    // in func getAlbums
    
    var allAlbums = [AlbumInfo]()
    defer {
      completion(allAlbums)
    }
  • 일반 앨범 정보 가져오기
    • PHFetchOptions: predicate를 이용하여 sorting, mediaType 등을 쿼리하는데 사용
    • predicate는 정규식이며, 내부 Constant로 정의해놓고 사용
    • 앨범을 가져올땐 PHAsset.fetchAssets(with:) 메소드를 사용
    // in func getAlbums
    let fetchOptions = PHFetchOptions()
    fetchOptions.predicate = Const.predicate(mediaType)
    let standardAlbum = PHAsset.fetchAssets(with: fetchOptions)
    allAlbums.append(
      .init(
        id: nil,
        name: Const.titleText(mediaType),
        count: standardAlbum.count,
        album: standardAlbum
      )
    )
    
// PhotoService에 선언된 상수
  private enum Const {
    static let titleText: (MediaType?) -> String = { mediaType in
      switch mediaType {
      case .all:
        return "이미지와 동영상"
      case .image:
        return "이미지"
      case .video:
        return "동영상"
      default:
        return "비어있는 타이틀"
      }
    }
    static let predicate: (MediaType) -> NSPredicate = { mediaType in
      let format = "mediaType == %d"
      switch mediaType {
      case .all:
        return .init(
          format: format + " || " + format,
          PHAssetMediaType.image.rawValue,
          PHAssetMediaType.video.rawValue
        )
      case .image:
        return .init(
          format: format,
          PHAssetMediaType.image.rawValue
        )
      case .video:
        return .init(
          format: format,
          PHAssetMediaType.video.rawValue
        )
      }
    }
    static let sortDescriptors = [
      NSSortDescriptor(key: "creationDate", ascending: false),
      NSSortDescriptor(key: "modificationDate", ascending: false)
    ]
  }
  • 스마트 앨범 조회
    // in func getAlbums
    
    let smartAlbums = PHAssetCollection.fetchAssetCollections(
      with: .smartAlbum,
      subtype: .any,
      options: PHFetchOptions()
    )
    guard 0 < smartAlbums.count else { return }
    smartAlbums.enumerateObjects { smartAlbum, index, pointer in
      guard index <= smartAlbums.count - 1 else {
        pointer.pointee = true
        return
      }
      if smartAlbum.estimatedAssetCount == NSNotFound {
        let fetchOptions = PHFetchOptions()
        fetchOptions.predicate = Const.predicate(mediaType)
        fetchOptions.sortDescriptors = Const.sortDescriptors
        let smartAlbums = PHAsset.fetchAssets(in: smartAlbum, options: fetchOptions)
        allAlbums.append(
          .init(
            id: smartAlbum.localIdentifier,
            name: smartAlbum.localizedTitle ?? Const.titleText(nil),
            count: smartAlbums.count,
            album: smartAlbums
          )
        )
      }
    }
  • getAlbums을 사용하는 쪽
    • album 버튼이 클릭되면 requestAlbum() 메소드 호출
    • `Photo Library Usage Description` Photo Library 권한을 요청한 후 권한이 동의되어 있으면 앨범들을 가져옴
    • 앨범들을 가져와서 PhotoViewController에 넘겨줌
  // ViewController.swift
  
  @objc private func requestAlbum() {
    self.requestAlbumAuthorization { isAuthorized in
      if isAuthorized {
        PhotoService.shared.getAlbums(mediaType: .image, completion: { [weak self] albums in
          DispatchQueue.main.async {
            let photoViewController = PhotoViewController(albums: albums)
            photoViewController.modalPresentationStyle = .fullScreen
            self?.present(photoViewController, animated: true)
          }
        })
      } else {
        self.showAlertGoToSetting(
          title: "현재 앨범 사용에 대한 접근 권한이 없습니다.",
          message: "설정 > {앱 이름} 탭에서 접근을 활성화 할 수 있습니다."
        )
      }
    }
  }
  • 앨범들의 정보를 가져오면 PickerView에 아래와 같이 데이터 입력이 가능

  • PhotoViewController에서 필요한 프로퍼티 선언
    • albums: ViewController에서 넘겨받는 데이터
    • currentAlbumIndex: PickerView를 통해 인덱스 값이 변경될때마다 didSet에서 해당 인덱스에 해당되는 앨범을 가져와서, phAsset형태로 가져옴 (getPHAsset 메소드는 아래에서 계속 설명)
    • currentAlbum: computed property이고 현재 pickerView로 선택된 앨범을 가져오는 것
    • phAssets: 사진값들이며, 해당 값을 collectionView의 cell에서 사용
  // PhotoViewController
  
  private var albums: [AlbumInfo]
  private var currentAlbumIndex = 0 {
    didSet {
      PhotoService.shared.getPHAssets(album: self.albums[self.currentAlbumIndex].album) { [weak self] phAssets in
        self?.phAssets = phAssets
      }
    }
  }
  private var currentAlbum: PHFetchResult<PHAsset>? {
    guard self.currentAlbumIndex <= self.albums.count - 1 else { return nil }
    return self.albums[self.currentAlbumIndex].album
  }
  private var phAssets = [PHAsset]() {
    didSet {
      DispatchQueue.main.async {
        self.collectionView.reloadData()
      }
    }
  }
  • PhotoService의 getPHAssets(album:) 메소드 구현
    • 위에서 구현했던 getAlbums()을 통해 앨범정보를 가져오면, 그 앨범에서 사진들을 가져와야 하므로 사진들을 가져오는 메소드
    • 앨범에서 가져오는 사진은 PHAsset형태라고 기억
  // PhotoService
  
  func getPHAssets(album: PHFetchResult<PHAsset>, completion: @escaping ([PHAsset]) -> Void) {
    guard 0 < album.count else { return }
    var phAssets = [PHAsset]()
    
    album.enumerateObjects { asset, index, stopPointer in
      guard index <= album.count - 1 else {
        stopPointer.pointee = true
        return
      }
      phAssets.append(asset)
    }
    
    completion(phAssets)
  }
  • 사진 데이터들 (phAssets)을 가져왔고, 이 사진 데이터들을 UIImage로 변경한 다음 cell에 적용이 필요
    • 아래처럼 cell에 UIImage를 넣어서 셀에 UIImage가 반영되도록, PhotoService에 fetchImage()메소드가 필요
    • fetchImage(): PHAsset 형을 UIImage로 변경하는 메소드
extension PhotoViewController: UICollectionViewDataSource {
  func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    self.phAssets.count
  }
  func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    guard
      let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PhotoCell.id, for: indexPath) as? PhotoCell
    else { fatalError() }
    
    PhotoService.shared.fetchImage(
      asset: self.phAssets[indexPath.item],
      size: .init(width: Const.length * Const.scale, height: Const.length * Const.scale),
      contentMode: .aspectFit
    ) { [weak cell] image in
      DispatchQueue.main.async {
        cell?.prepare(image: image)
      }
    }
    
    return cell
  }
}
  • 캐싱을 사용하기 위해서 PhotoService내부에 PHCachingImageManager를 선언하여, 이 인스턴스를 통해 이미지를 요청하여 UIImage 획득
  let imageManager = PHCachingImageManager()
  
  func fetchImage(
    asset: PHAsset,
    size: CGSize,
    contentMode: PHImageContentMode,
    completion: @escaping (UIImage) -> Void
  ) {
    let option = PHImageRequestOptions()
    option.isNetworkAccessAllowed = true // for icloud
    option.deliveryMode = .highQualityFormat
    
    self.imageManager.requestImage(
      for: asset,
      targetSize: size,
      contentMode: contentMode,
      options: option
    ) { image, _ in
      guard let image = image else { return }
      completion(image)
    }
  }

* 전체 코드: https://github.com/JK0369/ExPhotoLibrary

Comments