Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
Tags
- Xcode
- collectionview
- combine
- Clean Code
- MVVM
- UICollectionView
- Refactoring
- clean architecture
- swiftUI
- rxswift
- 리펙토링
- uitableview
- map
- Protocol
- uiscrollview
- 클린 코드
- 리펙터링
- ios
- UITextView
- 애니메이션
- ribs
- swift documentation
- Human interface guide
- Observable
- 리팩토링
- HIG
- 스위프트
- RxCocoa
- SWIFT
- tableView
Archives
- Today
- Total
김종권의 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로 이루어진 화면
- 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)
}
}
'iOS framework' 카테고리의 다른 글
Comments