Je dois obtenir et montrer les 3 dernières photos prises à partir de la photothèque sur l'événement viewDidload sans aucun clic.
Après cette étape, je devrais obtenir d'autres photos 3 par 3 lorsque je fais défiler la vue de défilement.
Connaissez-vous la bonne façon de le faire avec Swift? Merci.
Voici une solution utilisant le framework Photos
disponible pour les appareils iOS 8+:
import Photos
class ViewController: UIViewController {
var images:[UIImage] = []
func fetchPhotos () {
// Sort the images by descending creation date and fetch the first 3
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key:"creationDate", ascending: false)]
fetchOptions.fetchLimit = 3
// Fetch the image assets
let fetchResult: PHFetchResult = PHAsset.fetchAssets(with: PHAssetMediaType.image, options: fetchOptions)
// If the fetch result isn't empty,
// proceed with the image request
if fetchResult.count > 0 {
let totalImageCountNeeded = 3 // <-- The number of images to fetch
fetchPhotoAtIndex(0, totalImageCountNeeded, fetchResult)
}
}
// Repeatedly call the following method while incrementing
// the index until all the photos are fetched
func fetchPhotoAtIndex(_ index:Int, _ totalImageCountNeeded: Int, _ fetchResult: PHFetchResult<PHAsset>) {
// Note that if the request is not set to synchronous
// the requestImageForAsset will return both the image
// and thumbnail; by setting synchronous to true it
// will return just the thumbnail
let requestOptions = PHImageRequestOptions()
requestOptions.isSynchronous = true
// Perform the image request
PHImageManager.default().requestImage(for: fetchResult.object(at: index) as PHAsset, targetSize: view.frame.size, contentMode: PHImageContentMode.aspectFill, options: requestOptions, resultHandler: { (image, _) in
if let image = image {
// Add the returned image to your array
self.images += [image]
}
// If you haven't already reached the first
// index of the fetch result and if you haven't
// already stored all of the images you need,
// perform the fetch request again with an
// incremented index
if index + 1 < fetchResult.count && self.images.count < totalImageCountNeeded {
self.fetchPhotoAtIndex(index + 1, totalImageCountNeeded, fetchResult)
} else {
// Else you have completed creating your array
print("Completed array: \(self.images)")
}
})
}
}
Ajouter à Info.plist
<key>NSPhotoLibraryUsageDescription</key>
<string>{bla-bla-bla}</string>
AtomicArray ici: https://stackoverflow.com/a/54565351/4488252
import UIKit
import Photos
enum PhotoAlbumViewModel {
case regular(id: Int, title: String, count: Int, image: UIImage, isSelected: Bool)
case allPhotos(id: Int, title: String, count: Int, image: UIImage, isSelected: Bool)
var id: Int { switch self { case .regular(let params), .allPhotos(let params): return params.id } }
var count: Int { switch self { case .regular(let params), .allPhotos(let params): return params.count } }
var title: String { switch self { case .regular(let params), .allPhotos(let params): return params.title } }
}
class PhotoService {
internal lazy var imageManager = PHCachingImageManager()
private lazy var queue = DispatchQueue(label: "PhotoService_queue",
qos: .default, attributes: .concurrent,
autoreleaseFrequency: .workItem, target: nil)
private lazy var getImagesQueue = DispatchQueue(label: "PhotoService_getImagesQueue",
qos: .userInteractive, attributes: [],
autoreleaseFrequency: .inherit, target: nil)
private lazy var thumbnailSize = CGSize(width: 200, height: 200)
private lazy var imageAlbumsIds = AtomicArray<Int>()
private let getImageSemaphore = DispatchSemaphore(value: 12)
typealias AlbumData = (fetchResult: PHFetchResult<PHAsset>, assetCollection: PHAssetCollection?)
private let _cachedAlbumsDataSemaphore = DispatchSemaphore(value: 1)
private lazy var _cachedAlbumsData = [Int: AlbumData]()
deinit {
print("____ PhotoServiceImpl deinited")
imageManager.stopCachingImagesForAllAssets()
}
}
// albums
extension PhotoService {
private func getAlbumData(id: Int, completion: ((AlbumData?) -> Void)?) {
_ = _cachedAlbumsDataSemaphore.wait(timeout: .now() + .seconds(3))
if let cachedAlbum = _cachedAlbumsData[id] {
completion?(cachedAlbum)
_cachedAlbumsDataSemaphore.signal()
return
} else {
_cachedAlbumsDataSemaphore.signal()
}
var result: AlbumData? = nil
switch id {
case 0:
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)]
let allPhotos = PHAsset.fetchAssets(with: .image, options: fetchOptions)
result = (allPhotos, nil)
default:
let collections = getAllAlbumsAssetCollections()
let id = id - 1
if id < collections.count {
_fetchAssets(in: collections[id]) { fetchResult in
result = (fetchResult, collections[id])
}
}
}
guard let _result = result else { completion?(nil); return }
_ = _cachedAlbumsDataSemaphore.wait(timeout: .now() + .seconds(3))
_cachedAlbumsData[id] = _result
_cachedAlbumsDataSemaphore.signal()
completion?(_result)
}
private func getAllAlbumsAssetCollections() -> PHFetchResult<PHAssetCollection> {
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "endDate", ascending: true)]
fetchOptions.predicate = NSPredicate(format: "estimatedAssetCount > 0")
return PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: fetchOptions)
}
func getAllAlbums(completion: (([PhotoAlbumViewModel])->Void)?) {
queue.async { [weak self] in
guard let self = self else { return }
var viewModels = AtomicArray<PhotoAlbumViewModel>()
var allPhotosAlbumViewModel: PhotoAlbumViewModel?
let dispatchGroup = DispatchGroup()
dispatchGroup.enter()
self.getAlbumData(id: 0) { data in
guard let data = data, let asset = data.fetchResult.lastObject else { dispatchGroup.leave(); return }
self._fetchImage(from: asset, userInfo: nil, targetSize: self.thumbnailSize,
deliveryMode: .fastFormat, resizeMode: .fast) { [weak self] (image, _) in
guard let self = self, let image = image else { dispatchGroup.leave(); return }
allPhotosAlbumViewModel = .allPhotos(id: 0, title: "All Photos",
count: data.fetchResult.count,
image: image, isSelected: false)
self.imageAlbumsIds.append(0)
dispatchGroup.leave()
}
}
let numberOfAlbums = self.getAllAlbumsAssetCollections().count + 1
for id in 1 ..< numberOfAlbums {
dispatchGroup.enter()
self.getAlbumData(id: id) { [weak self] data in
guard let self = self else { return }
guard let assetCollection = data?.assetCollection else { dispatchGroup.leave(); return }
self.imageAlbumsIds.append(id)
self.getAlbumViewModel(id: id, collection: assetCollection) { [weak self] model in
guard let self = self else { return }
defer { dispatchGroup.leave() }
guard let model = model else { return }
viewModels.append(model)
}
}
}
_ = dispatchGroup.wait(timeout: .now() + .seconds(3))
var _viewModels = [PhotoAlbumViewModel]()
if let allPhotosAlbumViewModel = allPhotosAlbumViewModel {
_viewModels.append(allPhotosAlbumViewModel)
}
_viewModels += viewModels.get()
DispatchQueue.main.async { completion?(_viewModels) }
}
}
private func getAlbumViewModel(id: Int, collection: PHAssetCollection, completion: ((PhotoAlbumViewModel?) -> Void)?) {
_fetchAssets(in: collection) { [weak self] fetchResult in
guard let self = self, let asset = fetchResult.lastObject else { completion?(nil); return }
self._fetchImage(from: asset, userInfo: nil, targetSize: self.thumbnailSize,
deliveryMode: .fastFormat, resizeMode: .fast) { (image, nil) in
guard let image = image else { completion?(nil); return }
completion?(.regular(id: id,
title: collection.localizedTitle ?? "",
count: collection.estimatedAssetCount,
image: image, isSelected: false))
}
}
}
}
// fetch
extension PhotoService {
fileprivate func _fetchImage(from photoAsset: PHAsset,
userInfo: [AnyHashable: Any]? = nil,
targetSize: CGSize, //= PHImageManagerMaximumSize,
deliveryMode: PHImageRequestOptionsDeliveryMode = .fastFormat,
resizeMode: PHImageRequestOptionsResizeMode,
completion: ((_ image: UIImage?, _ userInfo: [AnyHashable: Any]?) -> Void)?) {
// guard authorizationStatus() == .authorized else { completion(nil); return }
let options = PHImageRequestOptions()
options.resizeMode = resizeMode
options.isSynchronous = true
options.deliveryMode = deliveryMode
imageManager.requestImage(for: photoAsset,
targetSize: targetSize,
contentMode: .aspectFill,
options: options) { (image, info) -> Void in
guard let info = info,
let isImageDegraded = info[PHImageResultIsDegradedKey] as? Int,
isImageDegraded == 0 else { completion?(nil, nil); return }
completion?(image, userInfo)
}
}
private func _fetchAssets(in collection: PHAssetCollection, completion: @escaping (PHFetchResult<PHAsset>) -> Void) {
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)]
let assets = PHAsset.fetchAssets(in: collection, options: fetchOptions)
completion(assets)
}
private func fetchImage(from asset: PHAsset,
userInfo: [AnyHashable: Any]?,
targetSize: CGSize,
deliveryMode: PHImageRequestOptionsDeliveryMode,
resizeMode: PHImageRequestOptionsResizeMode,
completion: ((UIImage?, _ userInfo: [AnyHashable: Any]?) -> Void)?) {
queue.async { [weak self] in
self?._fetchImage(from: asset, userInfo: userInfo, targetSize: targetSize,
deliveryMode: deliveryMode, resizeMode: resizeMode) { (image, _) in
DispatchQueue.main.async { completion?(image, userInfo) }
}
}
}
func getImage(albumId: Int, index: Int,
userInfo: [AnyHashable: Any]?,
targetSize: CGSize,
deliveryMode: PHImageRequestOptionsDeliveryMode,
resizeMode: PHImageRequestOptionsResizeMode,
completion: ((_ image: UIImage?, _ userInfo: [AnyHashable: Any]?) -> Void)?) {
getImagesQueue.async { [weak self] in
guard let self = self else { return }
let indexPath = IndexPath(item: index, section: albumId)
self.getAlbumData(id: albumId) { data in
_ = self.getImageSemaphore.wait(timeout: .now() + .seconds(3))
guard let photoAsset = data?.fetchResult.object(at: index) else { self.getImageSemaphore.signal(); return }
self.fetchImage(from: photoAsset,
userInfo: userInfo,
targetSize: targetSize,
deliveryMode: deliveryMode,
resizeMode: resizeMode) { [weak self] (image, userInfo) in
defer { self?.getImageSemaphore.signal() }
completion?(image, userInfo)
}
}
}
}
}
private lazy var photoLibrary = PhotoService()
private var albums = [PhotoAlbumViewModel]()
//....
// Get albums
photoLibrary.getAllAlbums { [weak self] albums in
self?.albums = albums
// reload views
}
// Get photo
photoLibrary.getImage(albumId: albums[0].id,
index: 1, userInfo: nil,
targetSize: CGSize(width: 200, height: 200),
deliveryMode: .fastFormat,
resizeMode: .fast) { [weak self, weak cell] (image, userInfo) in
// reload views
}
ViewController.Swift
import UIKit
import Photos
class ViewController: UIViewController {
private weak var collectionView: UICollectionView?
var collectionViewFlowLayout: UICollectionViewFlowLayout? {
return collectionView?.collectionViewLayout as? UICollectionViewFlowLayout
}
private lazy var photoLibrary = PhotoService()
private lazy var numberOfElementsInRow = 4
private lazy var cellIdentifier = "cellIdentifier"
private lazy var supplementaryViewIdentifier = "supplementaryViewIdentifier"
private var albums = [PhotoAlbumViewModel]()
private lazy var cellsTags = [IndexPath: Int]()
private lazy var tagKey = "cellTag"
private lazy var thumbnailImageSize = CGSize(width: 200, height: 200)
override func viewDidLoad() {
let collectionViewFlowLayout = UICollectionViewFlowLayout()
collectionViewFlowLayout.minimumLineSpacing = 5
collectionViewFlowLayout.minimumInteritemSpacing = 5
let _numberOfElementsInRow = CGFloat(numberOfElementsInRow)
let allWidthBetwenCells = _numberOfElementsInRow == 0 ? 0 : collectionViewFlowLayout.minimumInteritemSpacing*(_numberOfElementsInRow-1)
let width = (view.frame.width - allWidthBetwenCells)/_numberOfElementsInRow
collectionViewFlowLayout.itemSize = CGSize(width: width, height: width)
collectionViewFlowLayout.headerReferenceSize = CGSize(width: 0, height: 40)
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewFlowLayout)
view.addSubview(collectionView)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
collectionView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor).isActive = true
collectionView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor).isActive = true
collectionView.register(CollectionViewCell.self, forCellWithReuseIdentifier: cellIdentifier)
collectionView.register(SupplementaryView.self,
forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
withReuseIdentifier: supplementaryViewIdentifier)
collectionView.backgroundColor = .white
self.collectionView = collectionView
collectionView.delegate = self
showAllPhotosButtonTouchedInside()
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "All", style: .done, target: self,
action: #selector(showAllPhotosButtonTouchedInside))
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "last 3", style: .done, target: self,
action: #selector(showLastSeveralPhotosButtonTouchedInside))
}
@objc func showAllPhotosButtonTouchedInside() {
photoLibrary.getAllAlbums { [weak self] albums in
self?.set(albums: albums)
if self?.collectionView?.dataSource == nil {
self?.collectionView?.dataSource = self
} else {
self?.collectionView?.reloadData()
}
}
}
@objc func showLastSeveralPhotosButtonTouchedInside() {
photoLibrary.getAllAlbums { [weak self] albums in
guard let firstAlbum = albums.first else { return }
var album: PhotoAlbumViewModel!
let maxPhotosToDisplay = 3
switch firstAlbum {
case .allPhotos(let id, let title, let count, let image, let isSelected):
let newCount = count > maxPhotosToDisplay ? maxPhotosToDisplay : count
album = .allPhotos(id: id, title: title, count: newCount, image: image, isSelected: isSelected)
case .regular(let id, let title, let count, let image, let isSelected):
let newCount = count > maxPhotosToDisplay ? maxPhotosToDisplay : count
album = .regular(id: id, title: title, count: newCount, image: image, isSelected: isSelected)
}
self?.set(albums: [album])
if self?.collectionView?.dataSource == nil {
self?.collectionView?.dataSource = self
} else {
self?.collectionView?.reloadData()
}
}
}
private func set(albums: [PhotoAlbumViewModel]) {
self.albums = albums
var counter = 0
for (section, album) in albums.enumerated() {
for row in 0..<album.count {
self.cellsTags[IndexPath(row: row, section: section)] = counter
counter += 1
}
}
}
}
extension ViewController: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int { return albums.count }
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return albums[section].count
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: supplementaryViewIdentifier, for: indexPath) as! SupplementaryView
header.label?.text = albums[indexPath.section].title
return header
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as! CollectionViewCell
let tag = cellsTags[indexPath]!
cell.tag = tag
photoLibrary.getImage(albumId: albums[indexPath.section].id,
index: indexPath.item, userInfo: [tagKey: tag],
targetSize: thumbnailImageSize,
deliveryMode: .fastFormat,
resizeMode: .fast) { [weak self, weak cell] (image, userInfo) in
guard let cell = cell, let tagKey = self?.tagKey,
let cellTag = userInfo?[tagKey] as? Int,
cellTag == cell.tag else { return }
cell.imageView?.image = image
}
return cell
}
}
extension ViewController: UICollectionViewDelegate {}
CollectionViewCell.Swift
import UIKit
class CollectionViewCell: UICollectionViewCell {
weak var imageView: UIImageView?
override init(frame: CGRect) {
super.init(frame: frame)
clipsToBounds = true
let imageView = UIImageView(frame: .zero)
imageView.contentMode = .scaleAspectFill
addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.topAnchor.constraint(equalTo: topAnchor).isActive = true
imageView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
imageView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
imageView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
self.imageView = imageView
backgroundColor = UIColor.lightGray.withAlphaComponent(0.3)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func prepareForReuse() {
super.prepareForReuse()
imageView?.image = nil
}
}
SupplementaryView.Swift
import UIKit
class SupplementaryView: UICollectionReusableView {
weak var label: UILabel?
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
let label = UILabel(frame: frame)
label.textColor = .black
addSubview(label)
self.label = label
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func prepareForReuse() {
super.prepareForReuse()
self.label?.text = nil
}
}
Voici une solution élégante et efficace en Swift 4.
En bref, nous demandons une fois les derniers éléments photo, puis les convertissons en image si nécessaire.
Première importation de la bibliothèque de photos:
import Photos
Créez ensuite une fonction pour récupérer les dernières photos prises:
func fetchLatestPhotos(forCount count: Int?) -> PHFetchResult<PHAsset> {
// Create fetch options.
let options = PHFetchOptions()
// If count limit is specified.
if let count = count { options.fetchLimit = count }
// Add sortDescriptor so the lastest photos will be returned.
let sortDescriptor = NSSortDescriptor(key: "creationDate", ascending: false)
options.sortDescriptors = [sortDescriptor]
// Fetch the photos.
return PHAsset.fetchAssets(with: .image, options: options)
}
Dans votre cas, vous voudrez peut-être récupérer suffisamment de photos à la fois (par exemple 50), puis stocker le résultat quelque part dans votre contrôleur de vue:
var latestPhotoAssetsFetched: PHFetchResult<PHAsset>? = nil
Dans viewDidLoad
:
self.latestPhotoAssetsFetched = self.fetchLatestPhotos(forCount: 50)
Enfin, demandez l'image au bon endroit (par exemple, une cellule de vue de collection):
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
/*
...your code to configure the cell...
*/
// Get the asset. If nothing, return the cell.
guard let asset = self.latestPhotoAssetsFetched?[indexPath.item] else {
return cell
}
// Here we bind the asset with the cell.
cell.representedAssetIdentifier = asset.localIdentifier
// Request the image.
PHImageManager.default().requestImage(for: asset,
targetSize: cell.imageView.frame.size,
contentMode: .aspectFill,
options: nil) { (image, _) in
// By the time the image is returned, the cell may has been recycled.
// We update the UI only when it is still on the screen.
if cell.representedAssetIdentifier == asset.localIdentifier {
cell.imageView.image = image
}
}
return cell
}
N'oubliez pas d'ajouter une propriété à votre cellule:
class PhotoCell: UICollectionViewCell {
var representedAssetIdentifier: String? = nil
}
Vous pouvez extraire les 3 dernières photos à l'aide de fonctions dans le cadre AssetsLibrary
. Vous devez d'abord ajouter le cadre au projet. La fonction suivante récupère les 3 dernières photos et appelle le bloc d'achèvement.
import AssetsLibrary
func getLatestPhotos(completion completionBlock : ([UIImage] -> ())) {
let library = ALAssetsLibrary()
var count = 0
var images : [UIImage] = []
var stopped = false
library.enumerateGroupsWithTypes(ALAssetsGroupSavedPhotos, usingBlock: { (group,var stop) -> Void in
group?.setAssetsFilter(ALAssetsFilter.allPhotos())
group?.enumerateAssetsWithOptions(NSEnumerationOptions.Reverse, usingBlock: {
(asset : ALAsset!, index, var stopEnumeration) -> Void in
if (!stopped)
{
if count >= 3
{
stopEnumeration.memory = ObjCBool(true)
stop.memory = ObjCBool(true)
completionBlock(images)
stopped = true
}
else
{
// For just the thumbnails use the following line.
let cgImage = asset.thumbnail().takeUnretainedValue()
// Use the following line for the full image.
let cgImage = asset.defaultRepresentation().fullScreenImage().takeUnretainedValue()
if let image = UIImage(CGImage: cgImage) {
images.append(image)
count += 1
}
}
}
})
},failureBlock : { error in
println(error)
})
}
La fonction ci-dessus peut être appelée comme ceci
getLatestPhotos(completion: { images in
println(images)
//Set Images in this block.
})
Voici la réponse de @Lindsey Scott mais dans Objective-C. Je mets les 9 dernières photos de Camera Roll dans une vue de collection:
-(void)fetchPhotoFromEndAtIndex:(int)index{
PHImageRequestOptions *options = [[PHImageRequestOptions alloc]init];
options.synchronous = YES;
PHFetchOptions *fetchOptions = [[PHFetchOptions alloc]init];
fetchOptions.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:YES]];
PHFetchResult *photos = [PHAsset fetchAssetsWithMediaType:PHAssetMediaTypeImage options:fetchOptions];
if (photos) {
[[PHImageManager defaultManager] requestImageForAsset:[photos objectAtIndex:photos.count -1 -index] targetSize:CGSizeMake(self.collectionView.frame.size.width/3, self.collectionView.frame.size.height/3) contentMode:PHImageContentModeAspectFill options:options resultHandler:^(UIImage *result, NSDictionary *info) {
[self.imagesArray addObject:result];
if (index + 1 < photos.count && self.imagesArray.count < 9) {
[self fetchPhotoFromEndAtIndex:index + 1];
}
}];
}
[self.collectionView reloadData];
}