I need to get and show last taken 3 photos from photo library on viewDidload event without any clicks.
After this step, I should get other photos 3 by 3 when I scroll the ScrollView.
Do you know the proper way to do this with swift? Thanks.
Here's a solution using the Photos framework available for devices 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)")
}
})
}
}
Details
Xcode 10.2 (10E125), Swift 5
Solution features
works in asynchronously and thread/queue safety
get albums ( + all photos album)
optimized for fast scrolling
get images with defined size
Info.plist
Add to Info.plist
<key>NSPhotoLibraryUsageDescription</key>
<string>{bla-bla-bla}</string>
Solution
AtomicArray here: 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)
}
}
}
}
}
Usage
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
}
Full Sample (collectionView with images from PhotoLibrary)
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
}
}
Storyboard
Results
Here's an elegant solution with efficiency in Swift 4.
In short, we request the latest photo assets once, then convert them into image when needed.
First import Photos Library:
import Photos
Then create a function to fetch the lastest photos taken:
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)
}
In your case you might want to fetch enough photos at once (for example 50), then store the result somewhere in your view controller:
var latestPhotoAssetsFetched: PHFetchResult<PHAsset>? = nil
In viewDidLoad:
self.latestPhotoAssetsFetched = self.fetchLatestPhotos(forCount: 50)
Finally request the image at the right place (for example, a collection view cell):
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
}
Remember to add a property to your cell:
class PhotoCell: UICollectionViewCell {
var representedAssetIdentifier: String? = nil
}
You can extract the 3 latest photos using functions in the AssetsLibrary framework. First you have to add the framework to the project. The following function retrieves the 3 latest photos and calls the completion block.
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)
})
}
The above function can be called like this
getLatestPhotos(completion: { images in
println(images)
//Set Images in this block.
})
Here is #Lindsey Scott's answer but in Objective-C. I am putting the last 9 photos from Camera Roll into a collection view:
-(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];
}
Related
Every time when I use pull to refresh, the Active Indicator and the application itself freeze for a moment. I found which method is causing this problem - getTracks(), if I remove it then everything works well (except that the table is not updated), but I can't figure out what's the matter, I've tried many options, but no result. Can anyone help me?
My code:
class LibraryViewController: UIViewController {
private let fetchManager = FetchManager()
private var tracks: [TrackModelProtocol] = []
...
private let refreshControl: UIRefreshControl = {
let refreshControl = UIRefreshControl()
refreshControl.addTarget(self, action: #selector(refreshTableView), for: .valueChanged)
return refreshControl
}()
...
override func viewDidLoad() {
super.viewDidLoad()
...
getTracks()
setupTableView()
tableView.refreshControl = refreshControl
}
...
private func getTracks() {
guard let tracks = fetchManager.setupTrackModels() else { return }
self.tracks = tracks
}
//pull to refresh
#objc private func refreshTableView(sender: UIRefreshControl) {
getTracks()
tableView.reloadData()
sender.endRefreshing()
}
...
}
...
class FetchManager {
var content: [URL] = []
private var currentDirectory : URL?
private var playerQueue: [AVPlayerItem] = []
private let audioPlayer = AudioPlayer()
private func fetchDataFromDeviceMemory() -> [URL]? {
var content: [URL] = []
if currentDirectory == nil {
currentDirectory = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
}
do {
let allURLs = try FileManager.default.contentsOfDirectory(at: currentDirectory!, includingPropertiesForKeys: nil, options: [.skipsHiddenFiles, .skipsPackageDescendants])
content = allURLs.filter{ $0.pathExtension == "mp3" || $0.pathExtension == "aac" || $0.pathExtension == "wav" }
} catch {
content = []
}
return content
}
func setupTrackModels() -> [TrackModelProtocol]? {
guard let content = fetchDataFromDeviceMemory() else { return [] }
self.content = content
playerQueue = audioPlayer.createPlayerQueue(from: content)
var tracks: [TrackModelProtocol] = []
for index in 0..<playerQueue.count {
var artwork: UIImage?
let playerItem = playerQueue[index]
let metadataList = playerItem.asset.metadata
for item in metadataList {
guard let key = item.commonKey?.rawValue, let value = item.value else{
continue
}
switch key {
case "artwork" where value is Data:
artwork = UIImage(data: value as! Data)
default:
continue
}
}
let track = TrackFromDeviceMemory(
fileName: content[index].lastPathComponent,
duration: playerQueue[index].asset.duration.seconds,
artwork: artwork,
url: content[index].absoluteURL)
tracks.append(track)
}
return tracks
}
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
if isSearchActive {
let deletedTrackURL = filteredTracks[indexPath.row].url
print(deletedTrackURL)
do {
try FileManager.default.removeItem(at: deletedTrackURL)
} catch {
print(error.localizedDescription)
}
getTracks()
searchController.isActive = false
// tableView.reloadData()
tableView.delegate?.tableView!(tableView, didSelectRowAt: IndexPath(row: 0, section: indexPath.section))
} else {
let deletedTrackURL = tracks[indexPath.row].url
print(deletedTrackURL)
do {
try FileManager.default.removeItem(at: deletedTrackURL)
} catch {
print(error.localizedDescription)
}
getTracks()
tableView.deleteRows(at: [indexPath], with: .automatic)
// tableView.reloadData()
Instead of returning an array with setupTrackModels, have you tried using a completion handler and perform the fetch on a global queue? Performing the fetch on the main queue itself might be the reason for freeze.
func setupTrackModels(completion: (_ models: [TrackModelProtocol])->Void) {
DispatchQueue.global().async {
guard let content = fetchDataFromDeviceMemory() else {
DispatchQueue.main.async {
completion([])
return
}
}
self.content = content
playerQueue = audioPlayer.createPlayerQueue(from: content)
var tracks: [TrackModelProtocol] = []
for index in 0..<playerQueue.count {
var artwork: UIImage?
let playerItem = playerQueue[index]
let metadataList = playerItem.asset.metadata
for item in metadataList {
guard let key = item.commonKey?.rawValue, let value = item.value else{
continue
}
switch key {
case "artwork" where value is Data:
artwork = UIImage(data: value as! Data)
default:
continue
}
}
let track = TrackFromDeviceMemory(
fileName: content[index].lastPathComponent,
duration: playerQueue[index].asset.duration.seconds,
artwork: artwork,
url: content[index].absoluteURL)
tracks.append(track)
}
DispatchQueue.main.async {
completion(tracks)
}
}
}
At the call site:
fetchManager.setupTrackModels { tracks in
refreshControl.endRefreshing()
self.tracks = tracks
tableView.reloadData()
}
I am loading the photos from a users photo album into a collection view similar to how is done in this Apple Sample project. I can not seem to track down why the memory is growing out of control. I use the suggested PHCachingImageManager but all that results are blurry images, freezing scrolling and memory growing out of control until the application crashes.
In my viewDidLoad I run the code below
PHPhotoLibrary.requestAuthorization { (status: PHAuthorizationStatus) in
print("photo authorization status: \(status)")
if status == .authorized && self.fetchResult == nil {
print("authorized")
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
var tempArr:[PHAsset] = []
self.fetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions)
guard let fetchResult = self.fetchResult else{
print("Fetch result is empty")
return
}
fetchResult.enumerateObjects({asset, index, stop in
tempArr.append(asset)
})
// self.assets = tempArr
self.imageManager.startCachingImages(for: tempArr, targetSize: PHImageManagerMaximumSize, contentMode: .aspectFill, options: nil)
tempArr.removeAll()
print("Asset count after initial fetch: \(self.assets?.count)")
DispatchQueue.main.async {
// Reload collection view once we've determined our Photos permissions.
print("inside of main queue reload")
PHPhotoLibrary.shared().register(self)
self.collectionView.delegate = self
self.collectionView.dataSource = self
self.collectionView.reloadData()
}
} else {
print("photo access denied")
self.displayPhotoAccessDeniedAlert()
}
}
and inside of cellForItemAt: I run the following code
cellForItemAt
guard let fetchResult = self.fetchResult else{
print("Fetch Result is empty")
return UICollectionViewCell()
}
let requestOptions = PHImageRequestOptions()
requestOptions.isSynchronous = false
requestOptions.deliveryMode = .highQualityFormat
//let scale = min(2.0, UIScreen.main.scale)
let scale = UIScreen.main.scale
let targetSize = CGSize(width: cell.bounds.width * scale, height: cell.bounds.height * scale)
// let asset = assets[indexPath.item]
let asset = fetchResult.object(at: indexPath.item)
let assetIdentifier = asset.localIdentifier
cell.representedAssetIdentifier = assetIdentifier
imageManager.requestImage(for: asset, targetSize: cell.frame.size,
contentMode: .aspectFill, options: requestOptions) { (image, hashable) in
if let loadedImage = image, let cellIdentifier = cell.representedAssetIdentifier {
// Verify that the cell still has the same asset identifier,
// so the image in a reused cell is not overwritten.
if cellIdentifier == assetIdentifier {
cell.imageView.image = loadedImage
}
}
}
I had a similar problem this week using the Apple Code which for others reference is available here Browsing & Modifying Photos
Memory usage was very high, and then if viewing a single item and returning to root, memory would spike and the example would crash.
As such from our experiments there were a few tweaks which improved performance.
Firstly when setting the thumbnailSize for the requestImage function:
open func requestImage(for asset: PHAsset, targetSize: CGSize, contentMode: PHImageContentMode, options: PHImageRequestOptions?, resultHandler: #escaping (UIImage?, [AnyHashable : Any]?) -> Void) -> PHImageRequestID
We set the scale like so instead of using the full size:
UIScreen.main.scale * 0.75
We also set the PHImageRequestOptions Resizing Mode to .fast.
As well as this we found that setting the following variables of the CollectionViewCell also helped somewhat:
layer.shouldRasterize = true
layer.rasterizationScale = UIScreen.main.scale
isOpaque = true
We also noticed that the updateCachedAssets() in the ScrollViewwDidScroll method was playing some part in this process so we removed that from this callback(rightly or wrongly).
And one final thing was the we kept a reference to the PHCachingImageManager for each cell and if it existed then we called:
open func cancelImageRequest(_ requestID: PHImageRequestID)
As such here is the code for our MediaCell:
extension MediaCell{
/// Populates The Cell From The PHAsset Data
///
/// - Parameter asset: PHAsset
func populateCellFrom(_ asset: PHAsset){
livePhotoBadgeImage = asset.mediaSubtypes.contains(.photoLive) ? PHLivePhotoView.livePhotoBadgeImage(options: .overContent) : nil
videoDuration = asset.mediaType == .video ? asset.duration.formattedString() : ""
representedAssetIdentifier = asset.localIdentifier
}
/// Shows The Activity Indicator When Downloading From The Cloud
func startAnimator(){
DispatchQueue.main.async {
self.activityIndicator.isHidden = false
self.activityIndicator.startAnimating()
}
}
/// Hides The Activity Indicator After The ICloud Asset Has Downloaded
func endAnimator(){
DispatchQueue.main.async {
self.activityIndicator.isHidden = true
self.activityIndicator.stopAnimating()
}
}
}
final class MediaCell: UICollectionViewCell, Animatable {
#IBOutlet private weak var imageView: UIImageView!
#IBOutlet private weak var livePhotoBadgeImageView: UIImageView!
#IBOutlet private weak var videoDurationLabel: UILabel!
#IBOutlet weak var activityIndicator: UIActivityIndicatorView!{
didSet{
activityIndicator.isHidden = true
}
}
var representedAssetIdentifier: String!
var requestIdentifier: PHImageRequestID!
var thumbnailImage: UIImage! {
didSet {
imageView.image = thumbnailImage
}
}
var livePhotoBadgeImage: UIImage! {
didSet {
livePhotoBadgeImageView.image = livePhotoBadgeImage
}
}
var videoDuration: String!{
didSet{
videoDurationLabel.text = videoDuration
}
}
//----------------
//MARK:- LifeCycle
//----------------
override func awakeFromNib() {
layer.shouldRasterize = true
layer.rasterizationScale = UIScreen.main.scale
isOpaque = true
}
override func prepareForReuse() {
super.prepareForReuse()
imageView.image = nil
representedAssetIdentifier = ""
requestIdentifier = nil
livePhotoBadgeImageView.image = nil
videoDuration = ""
activityIndicator.isHidden = true
activityIndicator.stopAnimating()
}
}
And the code for the cellForItem:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let asset = dataViewModel.assettAtIndexPath(indexPath)
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "mediaCell", for: indexPath) as! MediaCell
if let requestID = cell.requestIdentifier { imageManager.cancelImageRequest(requestID) }
cell.populateCellFrom(asset)
let options = PHImageRequestOptions()
options.resizeMode = .fast
options.isNetworkAccessAllowed = true
options.progressHandler = { (progress, error, stop, info) in
if progress == 0.0{
cell.startAnimator()
} else if progress == 1.0{
cell.endAnimator()
}
}
cell.requestIdentifier = imageManager.requestImage(for: asset, targetSize: thumbnailSize,
contentMode: .aspectFill, options: options,
resultHandler: { image, info in
if cell.representedAssetIdentifier == asset.localIdentifier {
cell.thumbnailImage = image
}
})
return cell
}
One additional area is in the updateCachedAssets() funtion. You are using:
self.imageManager.startCachingImages(for: tempArr, targetSize: PHImageManagerMaximumSize, contentMode: .aspectFill, options: nil)
It would probably be best to set a smaller size here e.g:
imageManager.startCachingImages(for: addedAssets,
targetSize: thumbnailSize, contentMode: .aspectFill, options: nil)
Whereby thumbnail size e.g:
/// Sets The Thumnail Image Size
private func setupThumbnailSize(){
let scale = isIpad ? UIScreen.main.scale : UIScreen.main.scale * 0.75
let cellSize = collectionViewFlowLayout.itemSize
thumbnailSize = CGSize(width: cellSize.width * scale, height: cellSize.height * scale)
}
All of these tweaks helped to ensure that the memory usage remained fair constant, and in our testing ensured that there were no exceptions thrown.
Hope it helps.
I want to send images to firebase and then load them into the chat bubble. Right now after a user selects an image it gets loaded into firebase and also JSQPhotoMediaItem shows the image in the chat. However the other user only sees an empty bubble and when I reload the view it also shows as a blank bubble on my end as well. How can I fix the empty bubble and fill it with my photo url from Firebase.
class ChatViewController: JSQMessagesViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
let incomingBubble = JSQMessagesBubbleImageFactory(bubble: UIImage.jsq_bubbleCompactTailless(), capInsets: UIEdgeInsets.zero).incomingMessagesBubbleImage(with: UIColor(white: 0.90, alpha: 1.0))
let incomingBubbleWithTail = JSQMessagesBubbleImageFactory().incomingMessagesBubbleImage(with: UIColor(white: 0.90, alpha: 1.0))
let outgoingBubble = JSQMessagesBubbleImageFactory(bubble: UIImage.jsq_bubbleCompactTailless(), capInsets: UIEdgeInsets.zero).outgoingMessagesBubbleImage(with: UIColor.red)
let outgoingBubbleWithTail = JSQMessagesBubbleImageFactory().outgoingMessagesBubbleImage(with: UIColor.red)
var messages:[JSQMessage]!
var conversation:Conversation!
var conversationKey:String!
var partner:Users!
var partnerImage:UIImage?
var downloadRef:DatabaseReference?
#objc func handleUploadTap(){
let imagePickerController = UIImagePickerController()
imagePickerController.allowsEditing = true
imagePickerController.delegate = self
present(imagePickerController, animated: true, completion: nil)
print("image tapped")
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
var selectedImageFromPicker: UIImage?
if let editedImage = info["UIImagePickerControllerEditedImage"] as? UIImage{
selectedImageFromPicker = editedImage
}else if let originalImage = info["UIImagePickerControllerOriginalImage"] as? UIImage{
selectedImageFromPicker = originalImage
}
if let selectedImage = selectedImageFromPicker{
let mediaItem = JSQPhotoMediaItem(image: nil)
mediaItem?.appliesMediaViewMaskAsOutgoing = true
mediaItem?.image = UIImage(data: UIImageJPEGRepresentation(selectedImage, 0.5)!)
let sendMessage = JSQMessage(senderId: senderId, displayName: self.senderId, media: mediaItem)
self.messages.append(sendMessage!)
self.finishSendingMessage()
uploadToFirebaseStorageUsingImage(image: selectedImage)
}
dismiss(animated: true, completion: nil)
}
private func uploadToFirebaseStorageUsingImage(image: UIImage){
let imageName = NSUUID().uuidString
let ref = Storage.storage().reference().child("message_images").child(imageName)
if let uploadData = UIImageJPEGRepresentation(image, 0.3){
ref.putData(uploadData, metadata: nil, completion: { (metadata, error) in
if error != nil {
print("failed to load:", error)
return
}
if let imageUrl = metadata?.downloadURL()?.absoluteString{
self.sendMessageWithImageUrl(imageUrl: imageUrl)
}
})}}
private func sendMessageWithImageUrl(imageUrl: String){
guard let user = currentUser else { return }
let ref = Database.database().reference().child("conversations/threads/\(conversation.key)").childByAutoId()
let messageObject = [
"text":" ",
"recipient": conversation.partner_uid,
"sender":user.uid,
"senderName": user.firstLastName,
"imageUrl":imageUrl,
"timestamp": [".sv":"timestamp"]
] as [String:Any]
ref.setValue(messageObject, withCompletionBlock: { error, ref in
})
return self.finishSendingMessage(animated: true)
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
dismiss(animated: true, completion: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.backBarButtonItem = UIBarButtonItem(title: " ", style: .plain, target: self, action: #selector(handleDismiss))
view.backgroundColor = UIColor(white: 1.0, alpha: 1.0)
self.senderDisplayName = ""
if let user = Auth.auth().currentUser {
self.senderId = user.uid
} else {
self.senderId = ""
}
messages = [JSQMessage]()
let addImage = self.inputToolbar.contentView.leftBarButtonItem
addImage?.isUserInteractionEnabled = true
addImage?.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleUploadTap)))
self.inputToolbar.contentView.rightBarButtonItem.setTitleColor(UIColor.red, for: .normal)
// self.inputToolbar.contentView.leftBarButtonItemWidth = 0
self.inputToolbar.contentView.textView.placeHolder = "New message"
self.inputToolbar.contentView.textView.keyboardAppearance = .light
//collectionView?.collectionViewLayout.incomingAvatarViewSize = CGSize(width: 32, height: 32)
collectionView?.collectionViewLayout.outgoingAvatarViewSize = .zero
collectionView?.collectionViewLayout.springinessEnabled = true
collectionView?.backgroundColor = UIColor(white: 1.0, alpha: 1.0)
collectionView?.reloadData()
title = partner.firstLastName
conversation.printAll()
downloadRef = Database.database().reference().child("conversations/threads/\(conversation.key)")
downloadMessages()
}
#objc func handleDismiss() {
self.dismiss(animated: true, completion: nil)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
conversation.printAll()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
downloadRef?.removeAllObservers()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.messages.count
}
override func collectionView(_ collectionView: JSQMessagesCollectionView!, messageDataForItemAt indexPath: IndexPath!) -> JSQMessageData! {
let data = self.messages[indexPath.row]
return data
}
override func collectionView(_ collectionView: JSQMessagesCollectionView!, messageBubbleImageDataForItemAt indexPath: IndexPath!) -> JSQMessageBubbleImageDataSource! {
let data = messages[indexPath.row]
switch(data.senderId) {
case self.senderId:
return self.outgoingBubble
default:
return self.incomingBubble
}
}
override func collectionView(_ collectionView: JSQMessagesCollectionView!, avatarImageDataForItemAt indexPath: IndexPath!) -> JSQMessageAvatarImageDataSource! {
let data = messages[indexPath.row]
switch(data.senderId) {
case self.senderId:
return nil
default:
if partnerImage != nil {
let image = JSQMessagesAvatarImageFactory.avatarImage(with: partnerImage!, diameter: 48)
return image
}
return nil
}
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = super.collectionView(collectionView, cellForItemAt: indexPath) as! JSQMessagesCollectionViewCell
let data = messages[indexPath.row]
switch(data.senderId) {
case self.senderId:
cell.textView?.textColor = UIColor.white
default:
cell.textView?.textColor = UIColor.black
}
return cell
}
override func collectionView
(_ collectionView: JSQMessagesCollectionView!, attributedTextForCellTopLabelAt indexPath: IndexPath!) -> NSAttributedString! {
let currentItem = self.messages[indexPath.item]
if indexPath.item == 0 && messages.count > 8 {
return JSQMessagesTimestampFormatter.shared().attributedTimestamp(for: currentItem.date)
}
if indexPath.item > 0 {
let prevItem = self.messages[indexPath.item-1]
let gap = currentItem.date.timeIntervalSince(prevItem.date)
if gap > 1800 {
return JSQMessagesTimestampFormatter.shared().attributedTimestamp(for: currentItem.date)
}
} else {
return JSQMessagesTimestampFormatter.shared().attributedTimestamp(for: currentItem.date)
}
return nil
}
override func collectionView(_ collectionView: JSQMessagesCollectionView!, layout collectionViewLayout: JSQMessagesCollectionViewFlowLayout!, heightForCellTopLabelAt indexPath: IndexPath!) -> CGFloat {
if indexPath.item == 0 && messages.count > 8 {
return kJSQMessagesCollectionViewCellLabelHeightDefault
}
if indexPath.item > 0 {
let currentItem = self.messages[indexPath.item]
let prevItem = self.messages[indexPath.item-1]
let gap = currentItem.date.timeIntervalSince(prevItem.date)
if gap > 1800 {
return kJSQMessagesCollectionViewCellLabelHeightDefault
}
if prevItem.senderId != currentItem.senderId {
return 1.0
} else {
return 0.0
}
} else {
return kJSQMessagesCollectionViewCellLabelHeightDefault
}
}
override func collectionView(_ collectionView: JSQMessagesCollectionView!, layout collectionViewLayout: JSQMessagesCollectionViewFlowLayout!, heightForCellBottomLabelAt indexPath: IndexPath!) -> CGFloat {
return 0.0
}
override func didPressSend(_ button: UIButton!, withMessageText text: String!, senderId: String!, senderDisplayName: String!, date: Date!) {
guard let user = currentUser else { return }
let ref = Database.database().reference().child("conversations/threads/\(conversation.key)").childByAutoId()
let messageObject = [
"recipient": conversation.partner_uid,
"sender":user.uid,
"senderName": user.firstLastName,
"text":text,
"timestamp": [".sv":"timestamp"],
"imageUrl": " "
] as [String:Any]
ref.setValue(messageObject, withCompletionBlock: { error, ref in
})
return self.finishSendingMessage(animated: true)
}
func downloadMessages() {
self.messages = []
downloadRef?.observe(.childAdded, with: { snapshot in
let dict = snapshot.value as! [String:AnyObject]
let recipient = dict["recipient"] as! String
let sender = dict["sender"] as! String
let text = dict["text"] as! String
let timestamp = dict["timestamp"] as! Double
let imageUrl = dict["imageUrl"] as! String
let date = NSDate(timeIntervalSince1970: timestamp/1000)
var img: UIImage?
let mediaItem = JSQPhotoMediaItem(image: nil)
mediaItem?.appliesMediaViewMaskAsOutgoing = (id == self.senderId)
let message = JSQMessage(senderId: sender, senderDisplayName: "", date: date as Date!, text: text, media: mediaItem)
if img != nil{
mediaItem?.image = img! as UIImage
self.collectionView!.reloadData()
}
self.messages.append(message!)
self.reloadMessagesView()
self.finishReceivingMessage(animated: true)
})
}
func reloadMessagesView() {
self.collectionView?.reloadData()
guard let user = Auth.auth().currentUser else{ return }
let ref = Database.database().reference().child("conversations/users/\(user.uid)/\(conversation.partner_uid)/seen")
ref.setValue(true)
}
}
in your function downloadMessages() you get the imageUrl
let imageUrl = dict["imageUrl"] as! String
but when you append the message to messages, you do nothing with that imageUrl?
let message = JSQMessage(senderId: sender, senderDisplayName: "", date: date as Date!, text: text )
self.messages.append(message!)
your JSQMessage with an image should include a mediaItem (JSQPhotoMediaItem, otherwise you are just loading an empty message
var img: UIImage?
let mediaItem = JSQPhotoMediaItem(image: nil) //not a string
mediaItem?.appliesMediaViewMaskAsOutgoing = (id == self.senderId)
message = JSQMessage(senderId: id, senderDisplayName: name, date: date, media: mediaItem)
//get img from firebase, with whatever method you use
if img != nil {
mediaItem?.image = img! as UIImage
self.collectionView!.reloadData()
}
self.messages.append(message)
//etc
You might want to track the upload task: StorageReference.putData return a UploadTask. Add an onCompleteListener (or onSuccessListener, I don't remember exactly) to that task, when it's done, you can retrieve the uri and send the message.
This is for android, but similar for ios
I am creating a messaging app. I have this lazy var for a UIView in my messaging app. The View holds a textfield, and 2 buttons. The view is created programmatically. It is declared in a Vc ChatLog which displays the messages and handles the messaging functionality. When choose to present ChatLog through present(chatLogController, animated: true) it works as expected. However, whenever I place ChatLog in a ContainerView and present it through an embedded segue, the Input view does not appear or get executed. What could cause this?
Here's a picture to better explain:
Parent VC:
import UIKit
import Firebase
var segueUser: messageUser!
class MessagesVC: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
messages.removeAll()
messagesDictionary.removeAll()
tableView.reloadData()
tableView.tableFooterView = UIView()
observeUserMessages()
// Do any additional setup after loading the view.
}
var messages = [Message]()
var messagesDictionary = [String: Message]()
func observeUserMessages() {
guard let uid = Auth.auth().currentUser?.uid else {
return
}
let ref = Database.database().reference().child("user-messages").child(uid)
ref.observe(.childAdded, with: { (snapshot) in
// print(snapshot)
let userId = snapshot.key
Database.database().reference().child("user-messages").child(uid).child(userId).observe(.childAdded, with: { (snapshot) in
let messageId = snapshot.key
//print(messageId)
self.fetchMessageWithMessageId(messageId)
}, withCancel: nil)
}, withCancel: nil)
ref.observe(.childRemoved, with: { (snapshot) in
//print(snapshot.key)
//print(self.messagesDictionary)
self.messagesDictionary.removeValue(forKey: snapshot.key)
self.attemptReloadOfTable()
}, withCancel: nil)
}
fileprivate func fetchMessageWithMessageId(_ messageId: String) {
let messagesReference = Database.database().reference().child("messages").child(messageId)
messagesReference.observeSingleEvent(of: .value, with: { (snapshot) in
//print(snapshot)
if let dictionary = snapshot.value as? [String: AnyObject] {
let message = Message(dictionary: dictionary)
if let chatPartnerId = message.chatPartnerId() {
self.messagesDictionary[chatPartnerId] = message
}
self.attemptReloadOfTable()
}
}, withCancel: nil)
}
fileprivate func attemptReloadOfTable() {
self.timer?.invalidate()
self.timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(self.handleReloadTable), userInfo: nil, repeats: false)
}
var timer: Timer?
func handleReloadTable() {
self.messages = Array(self.messagesDictionary.values)
self.messages.sort(by: { (message1, message2) -> Bool in
return (message1.timestamp?.int32Value)! > (message2.timestamp?.int32Value)!
})
//this will crash because of background thread, so lets call this on dispatch_async main thread
DispatchQueue.main.async(execute: {
self.tableView.reloadData()
})
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print(messages.count)
return messages.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "userMessageCell", for: indexPath) as! userMessageCell
let message = messages[indexPath.row]
cell.message = message
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let message = messages[indexPath.row]
print(message)
guard let chatPartnerId = message.chatPartnerId() else {
return
}
businessName = message.businessName!
let ref = Database.database().reference().child("businessSearch").child(chatPartnerId)
ref.observeSingleEvent(of: .value, with: { (snapshot) in
guard let dictionary = snapshot.value as? [String: AnyObject] else {
return
}
let user = messageUser(dictionary: dictionary)
segueUser = user
user.id = chatPartnerId
//self.showChatControllerForUser(user)
self.performSegue(withIdentifier: "toChatLog", sender: nil)
}, withCancel: nil)
}
func showChatControllerForUser(_ user: messageUser) {
let chatLogController = ChatLogController(collectionViewLayout: UICollectionViewFlowLayout())
chatLogController.user = user
present(chatLogController, animated: true)
//navigationController?.pushViewController(chatLogController, animated: true)
}
#IBAction func backButtonPressed(_ sender: Any) {
self.dismiss(animated: true, completion: nil)
}
}
SecondVC (created in Interface Builder):
import UIKit
import Firebase
import MobileCoreServices
import AVFoundation
class ChatLogFrameVC: UIViewController {
#IBOutlet weak var nameLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
//nameLabel.text = businessName
}
#IBAction func infoButtonPressed(_ sender: Any) {
}
#IBAction func backButtonPressed(_ sender: Any) {
self.dismiss(animated: true, completion: nil)
}
}
ChatLogVC:
import UIKit
import Firebase
import MobileCoreServices
import AVFoundation
class ChatLogController: UICollectionViewController, UITextFieldDelegate, UICollectionViewDelegateFlowLayout, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
var user: messageUser!
var messages = [Message]()
func observeMessages() {
guard let uid = Auth.auth().currentUser?.uid, let toId = user?.id else {
return
}
let userMessagesRef = Database.database().reference().child("user-messages").child(uid).child(toId)
userMessagesRef.observe(.childAdded, with: { (snapshot) in
let messageId = snapshot.key
let messagesRef = Database.database().reference().child("messages").child(messageId)
messagesRef.observeSingleEvent(of: .value, with: { (snapshot) in
guard let dictionary = snapshot.value as? [String: AnyObject] else {
return
}
self.messages.append(Message(dictionary: dictionary))
DispatchQueue.main.async(execute: {
self.collectionView?.reloadData()
//scroll to the last index
let indexPath = IndexPath(item: self.messages.count - 1, section: 0)
self.collectionView?.scrollToItem(at: indexPath, at: .bottom, animated: true)
})
}, withCancel: nil)
}, withCancel: nil)
}
let cellId = "cellId"
override func viewDidLoad() {
super.viewDidLoad()
self.navigationController?.navigationBar.isHidden = false
collectionView?.collectionViewLayout = UICollectionViewFlowLayout()
user = segueUser
observeMessages()
print("CHAT LOG IS RUNNING")
collectionView?.showsVerticalScrollIndicator = false
collectionView?.contentInset = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0)
// collectionView?.scrollIndicatorInsets = UIEdgeInsets(top: 0, left: 0, bottom: 50, right: 0)
collectionView?.alwaysBounceVertical = true
collectionView?.backgroundColor = UIColor.white
collectionView?.register(ChatMessageCell.self, forCellWithReuseIdentifier: cellId)
collectionView?.keyboardDismissMode = .interactive
//collectionView?.addSubview(topChatContainerView)
//collectionView?.bringSubview(toFront: topChatContainerView)
self.title = businessName
setupKeyboardObservers()
}
lazy var inputContainerView: ChatInputContainerView = {
let chatInputContainerView = ChatInputContainerView(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: 64))
chatInputContainerView.chatLogController = self
return chatInputContainerView
}()
func handleUploadTap() {
let imagePickerController = UIImagePickerController()
imagePickerController.allowsEditing = true
imagePickerController.delegate = self
imagePickerController.mediaTypes = [kUTTypeImage as String, kUTTypeMovie as String]
present(imagePickerController, animated: true, completion: nil)
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
if let videoUrl = info[UIImagePickerControllerMediaURL] as? URL {
//we selected a video
handleVideoSelectedForUrl(videoUrl)
} else {
//we selected an image
handleImageSelectedForInfo(info as [String : AnyObject])
}
dismiss(animated: true, completion: nil)
}
fileprivate func handleVideoSelectedForUrl(_ url: URL) {
let filename = UUID().uuidString + ".mov"
let uploadTask = Storage.storage().reference().child("message_movies").child(filename).putFile(from: url, metadata: nil, completion: { (metadata, error) in
if error != nil {
print("Failed upload of video:", error!)
return
}
if let videoUrl = metadata?.downloadURL()?.absoluteString {
if let thumbnailImage = self.thumbnailImageForFileUrl(url) {
self.uploadToFirebaseStorageUsingImage(thumbnailImage, completion: { (imageUrl) in
let properties: [String: AnyObject] = ["imageUrl": imageUrl as AnyObject, "imageWidth": thumbnailImage.size.width as AnyObject, "imageHeight": thumbnailImage.size.height as AnyObject, "videoUrl": videoUrl as AnyObject]
self.sendMessageWithProperties(properties)
})
}
}
})
uploadTask.observe(.progress) { (snapshot) in
if let completedUnitCount = snapshot.progress?.completedUnitCount {
self.navigationItem.title = String(completedUnitCount)
}
}
uploadTask.observe(.success) { (snapshot) in
self.navigationItem.title = self.user?.name
}
}
fileprivate func thumbnailImageForFileUrl(_ fileUrl: URL) -> UIImage? {
let asset = AVAsset(url: fileUrl)
let imageGenerator = AVAssetImageGenerator(asset: asset)
do {
let thumbnailCGImage = try imageGenerator.copyCGImage(at: CMTimeMake(1, 60), actualTime: nil)
return UIImage(cgImage: thumbnailCGImage)
} catch let err {
print(err)
}
return nil
}
fileprivate func handleImageSelectedForInfo(_ info: [String: AnyObject]) {
var selectedImageFromPicker: UIImage?
if let editedImage = info["UIImagePickerControllerEditedImage"] as? UIImage {
selectedImageFromPicker = editedImage
} else if let originalImage = info["UIImagePickerControllerOriginalImage"] as? UIImage {
selectedImageFromPicker = originalImage
}
if let selectedImage = selectedImageFromPicker {
uploadToFirebaseStorageUsingImage(selectedImage, completion: { (imageUrl) in
self.sendMessageWithImageUrl(imageUrl, image: selectedImage)
})
}
}
fileprivate func uploadToFirebaseStorageUsingImage(_ image: UIImage, completion: #escaping (_ imageUrl: String) -> ()) {
let imageName = UUID().uuidString
let ref = Storage.storage().reference().child("message_images").child(imageName)
if let uploadData = UIImageJPEGRepresentation(image, 0.8) {
ref.putData(uploadData, metadata: nil, completion: { (metadata, error) in
if error != nil {
print("Failed to upload image:", error!)
return
}
if let imageUrl = metadata?.downloadURL()?.absoluteString {
completion(imageUrl)
}
})
}
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
dismiss(animated: true, completion: nil)
}
override var inputAccessoryView: UIView? {
get {
return inputContainerView
}
}
override var canBecomeFirstResponder : Bool {
return true
}
func setupKeyboardObservers() {
NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardDidShow), name: NSNotification.Name.UIKeyboardDidShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardWillShow), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
//
NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardWillHide), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
}
func handleKeyboardDidShow() {
if messages.count > 0 {
let indexPath = IndexPath(item: messages.count - 1, section: 0)
collectionView?.scrollToItem(at: indexPath, at: .top, animated: true)
}
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
NotificationCenter.default.removeObserver(self)
}
func handleKeyboardWillShow(_ notification: Notification) {
let keyboardFrame = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as AnyObject).cgRectValue
let keyboardDuration = (notification.userInfo?[UIKeyboardAnimationDurationUserInfoKey] as AnyObject).doubleValue
containerViewBottomAnchor?.constant = -keyboardFrame!.height
UIView.animate(withDuration: keyboardDuration!, animations: {
self.view.layoutIfNeeded()
})
}
func handleKeyboardWillHide(_ notification: Notification) {
let keyboardDuration = (notification.userInfo?[UIKeyboardAnimationDurationUserInfoKey] as AnyObject).doubleValue
containerViewBottomAnchor?.constant = 0
UIView.animate(withDuration: keyboardDuration!, animations: {
self.view.layoutIfNeeded()
})
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return messages.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! ChatMessageCell
cell.chatLogController = self
let message = messages[indexPath.item]
cell.message = message
cell.textView.text = message.text
setupCell(cell, message: message)
if let text = message.text {
//a text message
cell.bubbleWidthAnchor?.constant = estimateFrameForText(text).width + 32
cell.textView.isHidden = false
} else if message.imageUrl != nil {
//fall in here if its an image message
cell.bubbleWidthAnchor?.constant = 200
cell.textView.isHidden = true
}
cell.playButton.isHidden = message.videoUrl == nil
return cell
}
fileprivate func setupCell(_ cell: ChatMessageCell, message: Message) {
if let profileImageUrl = self.user?.profileImageUrl {
cell.profileImageView.loadImageUsingCacheWithUrlString(profileImageUrl)
}
if message.fromId == Auth.auth().currentUser?.uid {
//outgoing blue
cell.bubbleView.backgroundColor = ChatMessageCell.blueColor
cell.textView.textColor = UIColor.white
cell.profileImageView.isHidden = true
cell.bubbleViewRightAnchor?.isActive = true
cell.bubbleViewLeftAnchor?.isActive = false
} else {
//incoming gray
cell.bubbleView.backgroundColor = UIColor(red:0.81, green:0.81, blue:0.81, alpha:1.0)
cell.textView.textColor = UIColor.black
cell.profileImageView.isHidden = false
cell.bubbleViewRightAnchor?.isActive = false
cell.bubbleViewLeftAnchor?.isActive = true
}
if let messageImageUrl = message.imageUrl {
cell.messageImageView.loadImageUsingCacheWithUrlString(messageImageUrl)
cell.messageImageView.isHidden = false
cell.bubbleView.backgroundColor = UIColor.clear
} else {
cell.messageImageView.isHidden = true
}
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
collectionView?.collectionViewLayout.invalidateLayout()
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
var height: CGFloat = 80
let message = messages[indexPath.item]
if let text = message.text {
height = estimateFrameForText(text).height + 20
} else if let imageWidth = message.imageWidth?.floatValue, let imageHeight = message.imageHeight?.floatValue {
// h1 / w1 = h2 / w2
// solve for h1
// h1 = h2 / w2 * w1
height = CGFloat(imageHeight / imageWidth * 200)
}
let width = UIScreen.main.bounds.width
return CGSize(width: width, height: height)
}
fileprivate func estimateFrameForText(_ text: String) -> CGRect {
let size = CGSize(width: 200, height: 1000)
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
return NSString(string: text).boundingRect(with: size, options: options, attributes: [NSFontAttributeName: (UIFont(name: "Avenir Next", size: 17))!], context: nil)
}
var containerViewBottomAnchor: NSLayoutConstraint?
func handleSend() {
let properties = ["message": inputContainerView.inputTextField.text!]
sendMessageWithProperties(properties as [String : AnyObject])
}
fileprivate func sendMessageWithImageUrl(_ imageUrl: String, image: UIImage) {
let properties: [String: AnyObject] = ["imageUrl": imageUrl as AnyObject, "imageWidth": image.size.width as AnyObject, "imageHeight": image.size.height as AnyObject]
sendMessageWithProperties(properties)
}
fileprivate func sendMessageWithProperties(_ properties: [String: AnyObject]) {
let ref = Database.database().reference().child("messages")
let childRef = ref.childByAutoId()
let toId = user!.id!
let fromId = Auth.auth().currentUser!.uid
let timestamp = Int(Date().timeIntervalSince1970)
var values: [String: AnyObject] = ["toID": toId as AnyObject, "fromID": fromId as AnyObject, "timestamp": timestamp as AnyObject, "firstName": firstName as AnyObject,"businessName": businessName as AnyObject]
//append properties dictionary onto values somehow??
//key $0, value $1
properties.forEach({values[$0] = $1})
childRef.updateChildValues(values) { (error, ref) in
if error != nil {
print(error!)
return
}
self.inputContainerView.inputTextField.text = nil
let userMessagesRef = Database.database().reference().child("user-messages").child(fromId).child(toId)
let messageId = childRef.key
userMessagesRef.updateChildValues([messageId: 1])
let recipientUserMessagesRef = Database.database().reference().child("user-messages").child(toId).child(fromId)
recipientUserMessagesRef.updateChildValues([messageId: 1])
}
}
var startingFrame: CGRect?
var blackBackgroundView: UIView?
var startingImageView: UIImageView?
//my custom zooming logic
func performZoomInForStartingImageView(_ startingImageView: UIImageView) {
self.startingImageView = startingImageView
self.startingImageView?.isHidden = true
startingFrame = startingImageView.superview?.convert(startingImageView.frame, to: nil)
let zoomingImageView = UIImageView(frame: startingFrame!)
zoomingImageView.backgroundColor = UIColor.red
zoomingImageView.image = startingImageView.image
zoomingImageView.isUserInteractionEnabled = true
zoomingImageView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleZoomOut)))
if let keyWindow = UIApplication.shared.keyWindow {
blackBackgroundView = UIView(frame: keyWindow.frame)
blackBackgroundView?.backgroundColor = UIColor.black
blackBackgroundView?.alpha = 0
keyWindow.addSubview(blackBackgroundView!)
keyWindow.addSubview(zoomingImageView)
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
self.blackBackgroundView?.alpha = 1
self.inputContainerView.alpha = 0
// math?
// h2 / w1 = h1 / w1
// h2 = h1 / w1 * w1
let height = self.startingFrame!.height / self.startingFrame!.width * keyWindow.frame.width
zoomingImageView.frame = CGRect(x: 0, y: 0, width: keyWindow.frame.width, height: height)
zoomingImageView.center = keyWindow.center
}, completion: { (completed) in
// do nothing
})
}
}
func handleZoomOut(_ tapGesture: UITapGestureRecognizer) {
if let zoomOutImageView = tapGesture.view {
//need to animate back out to controller
zoomOutImageView.layer.cornerRadius = 16
zoomOutImageView.clipsToBounds = true
UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 1, options: .curveEaseOut, animations: {
zoomOutImageView.frame = self.startingFrame!
self.blackBackgroundView?.alpha = 0
self.inputContainerView.alpha = 1
}, completion: { (completed) in
zoomOutImageView.removeFromSuperview()
self.startingImageView?.isHidden = false
})
}
}
}
The app can create a custom album in the standard iOS photos application, but I have been unable to find a way for the app to gather all the images from that album to be displayed within the app.
Currently, the app is able to gather images from all the albums, just not one is particular.
let resultCollections = PHAssetCollection.fetchAssetCollectionsWithType(
.Album,
subtype: .AlbumRegular,
options: nil)
resultCollections.enumerateObjectsUsingBlock({
(object, index, stop) -> Void in
let collection = object as! PHAssetCollection
let result = PHAsset.fetchAssetsInAssetCollection(collection, options: nil)
result.enumerateObjectsUsingBlock({
(object, index, stop) -> Void in
let asset = object as! PHAsset
self.images.append(asset)
})
})
I have seen other questions that might be marked as duplicates, however the majority of them are talking about opening a UIPickerView to a custom album. This is a possible duplicate of How to fetch all images from custom Photo Album - swift however, it was never answered.
So, how can an iOS app gather all images from a particular photos album?
Add fetchOptions like below
let fetchOptions = PHFetchOptions()
fetchOptions.predicate = NSPredicate(format: "title = %#", YourAlbumTitle)
let resultCollections = PHAssetCollection.fetchAssetCollectionsWithType(.Album, subtype: .AlbumRegular, options: fetchOptions)
Actually, the album title isn't a unique value, they can be duplicated. so I recommend using localIdentifier like below if your app access multiple albums.
let fetchOptions = PHFetchOptions()
fetchOptions.predicate = NSPredicate(format: "localIdentifier = %#", YourAlbumLocalIdentifier)
let resultCollections = PHAssetCollection.fetchAssetCollectionsWithType(.Album, subtype: .AlbumRegular, options: fetchOptions)
Working Code For Swift 4
My Answer might help you and others(https://stackoverflow.com/a/35178022/4795651) but then also adding the code here..!!
import Photos
func fetchCustomAlbumPhotos()
{
let albumName = "Album Name Here"
var assetCollection = PHAssetCollection()
var albumFound = Bool()
var photoAssets = PHFetchResult<AnyObject>()
let fetchOptions = PHFetchOptions()
fetchOptions.predicate = NSPredicate(format: "title = %#", albumName)
let collection:PHFetchResult = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: fetchOptions)
if let firstObject = collection.firstObject{
//found the album
assetCollection = firstObject
albumFound = true
}
else { albumFound = false }
_ = collection.count
photoAssets = PHAsset.fetchAssets(in: assetCollection, options: nil) as! PHFetchResult<AnyObject>
let imageManager = PHCachingImageManager()
photoAssets.enumerateObjects{(object: AnyObject!,
count: Int,
stop: UnsafeMutablePointer<ObjCBool>) in
if object is PHAsset{
let asset = object as! PHAsset
print("Inside If object is PHAsset, This is number 1")
let imageSize = CGSize(width: asset.pixelWidth,
height: asset.pixelHeight)
/* For faster performance, and maybe degraded image */
let options = PHImageRequestOptions()
options.deliveryMode = .fastFormat
options.isSynchronous = true
imageManager.requestImage(for: asset,
targetSize: imageSize,
contentMode: .aspectFill,
options: options,
resultHandler: {
(image, info) -> Void in
self.photo = image!
/* The image is now available to us */
self.addImgToArray(uploadImage: self.photo!)
print("enum for image, This is number 2")
})
}
}
}
func addImgToArray(uploadImage:UIImage)
{
self.images.append(uploadImage)
}
For Swift 2.1
import Photos
func FetchCustomAlbumPhotos()
{
var albumName = "SwiftAlbum"
var assetCollection = PHAssetCollection()
var albumFound = Bool()
var photoAssets = PHFetchResult()
let fetchOptions = PHFetchOptions()
fetchOptions.predicate = NSPredicate(format: "title = %#", albumName)
let collection:PHFetchResult = PHAssetCollection.fetchAssetCollectionsWithType(.Album, subtype: .Any, options: fetchOptions)
if let first_Obj:AnyObject = collection.firstObject{
//found the album
assetCollection = collection.firstObject as! PHAssetCollection
albumFound = true
}
else { albumFound = false }
var i = collection.count
photoAssets = PHAsset.fetchAssetsInAssetCollection(assetCollection, options: nil)
let imageManager = PHCachingImageManager()
// let imageManager = PHImageManager.defaultManager()
photoAssets.enumerateObjectsUsingBlock{(object: AnyObject!,
count: Int,
stop: UnsafeMutablePointer<ObjCBool>) in
if object is PHAsset{
let asset = object as! PHAsset
print("Inside If object is PHAsset, This is number 1")
let imageSize = CGSize(width: asset.pixelWidth,
height: asset.pixelHeight)
/* For faster performance, and maybe degraded image */
let options = PHImageRequestOptions()
options.deliveryMode = .FastFormat
options.synchronous = true
imageManager.requestImageForAsset(asset,
targetSize: imageSize,
contentMode: .AspectFill,
options: options,
resultHandler: {
(image, info) -> Void in
self.photo = image!
/* The image is now available to us */
self.addImgToArray(self.photo)
print("enum for image, This is number 2")
})
}
}
}
func addImgToArray(uploadImage:UIImage)
{
self.images.append(uploadImage)
}
class AlbumModel {
let name:String
let count:Int
let asset:NSMutableArray
init(name:String, count:Int, asset:NSMutableArray) {
self.name = name
self.count = count
self.asset = asset
}
}
class yourCustomCell: UITableViewCell {
//MARK:- Properties
#IBOutlet weak var collectionView: UICollectionView!
//MARK:- initialization methods
override func awakeFromNib() {
super.awakeFromNib()
// setupView()
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
//MARK:- Setup collectionView datasource and delegate
func setCollectionViewDataSourceDelegate<T:UICollectionViewDataSource & UICollectionViewDelegate>(dataSourceDelegate: T, forRow row: Int) {
collectionView.delegate = dataSourceDelegate
collectionView.dataSource = dataSourceDelegate
collectionView.tag = row
collectionView.reloadData()
}
}
class ViewController: UIViewController {
var tablearray = NSMutableArray()
func getAssetThumbnail(asset: PHAsset) -> UIImage {
let manager = PHImageManager.default()
let option = PHImageRequestOptions()
var thumbnail = UIImage()
option.isSynchronous = true
manager.requestImage(for: asset, targetSize: CGSize(width: 100, height: 100), contentMode: .aspectFit, options: option, resultHandler: {(result, info)->Void in
thumbnail = result!
})
return thumbnail
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
//print(UnladenSwallow.unknown)
let fetchOptions = PHFetchOptions()
let smartAlbums = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .any, options: fetchOptions)
let topLevelfetchOptions = PHFetchOptions()
let topLevelUserCollections = PHCollectionList.fetchTopLevelUserCollections(with: topLevelfetchOptions)
let allAlbums = [topLevelUserCollections, smartAlbums]
var name = ""
smartAlbums.enumerateObjects({
if let collection = $0.0 as? PHAssetCollection{
name = collection.localizedTitle!
let image_arry = NSMutableArray()
let result = PHAsset.fetchAssets(in: collection, options: nil)
result.enumerateObjects({ (object, index, stop) -> Void in
let asset = object
image_arry.add(self.getAssetThumbnail(asset: asset))
})
let newAlbum = AlbumModel(name: name, count: collection.estimatedAssetCount, asset:image_arry)
self.tablearray.add(newAlbum)
}
})
topLevelUserCollections.enumerateObjects({
if let collection = $0.0 as? PHAssetCollection{
name = collection.localizedTitle!
let image_arry = NSMutableArray()
let result = PHAsset.fetchAssets(in: collection, options: nil)
result.enumerateObjects({ (object, index, stop) -> Void in
let asset = object
image_arry.add(self.getAssetThumbnail(asset: asset))
})
let newAlbum = AlbumModel(name: name, count: collection.estimatedAssetCount, asset:image_arry)
self.tablearray.add(newAlbum)
}
})
print(self.tablearray)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
extension ViewController: UITableViewDataSource,UITableViewDelegate{
func numberOfSections(in tableView: UITableView) -> Int{
return self.tablearray.count
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
let album = self.tablearray[section] as! AlbumModel
return album.name
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! yourCustomCell
cell.setCollectionViewDataSourceDelegate(dataSourceDelegate: self, forRow: indexPath.section)
return cell
}
}
extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
let album = self.tablearray[collectionView.tag] as! AlbumModel
print("count = \(album.asset.count)")
return album.asset.count;
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let album = self.tablearray[collectionView.tag] as! AlbumModel
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "col", for: indexPath)
let img = cell.viewWithTag(111) as! UIImageView
img.image = album.asset.object(at: indexPath.row) as? UIImage
return cell
}
}
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: IndexPath) {
print("get selected collectionview itemindex \(indexPath.row)")
}