DidSelectItemAt Collection view issue - ios

I'm having problem with didSelectItemAt in a UICollectionViewCell. I'm using a third party framework to select multiple photos from the library. The selected assets are then transferred and stored in a array of PHAssets. I use a small script for custom preview of the images when tapped on them. (It basically expands the image to full screen and put in and then you can swipe up or down to dismiss it). The problem is not with the images, they're properly displayed in my custom UICollectionViewCell. The problem is when I tap on them. For some reason it gives the wrong image and I'm not sure why.
var cameraPhotoUIImage: UIImage?
var assets = [PHAsset]()
lazy var assetsTurnedIntoImages =
{
return [UIImage]()
}()
lazy var imageManager = {
return PHCachingImageManager()
}()
extension PhotoVC : UICollectionViewDataSource, UICollectionViewDelegate
{
func setupCollectionView()
{
collectionView.dataSource = self
collectionView.delegate = self
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PhotoPostCVCell", for: indexPath) as! PhotoPostCVCell
if let takenImage = cameraPhotoUIImage
{
cell.cellImage.image = takenImage
}
if assets.count > 0
{
let asset = assets[indexPath.row]
imageManager.requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .aspectFill, options: nil)
{ (image, info) in
cell.cellImage.contentMode = .scaleAspectFill
cell.cellImage.image = image!
self.assetsTurnedIntoImages.append(image!)
}
}
return cell
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
{
if assets.count > 0
{
return assets.count
}
else
{
return 1
}
}
// MARK: Preview Selected Image
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let postStoryboard = UIStoryboard(name: Storyboard_Post, bundle:nil)
if let destinationVC = postStoryboard.instantiateViewController(withIdentifier: "PreviewImageVC") as? PreviewImageVC
{
destinationVC.allowedDismissDirection = .both
destinationVC.maskType = .clear
if cameraPhotoUIImage != nil
{
destinationVC.transferedImageToPreview.image = cameraPhotoUIImage
destinationVC.showInteractive()
}
if assetsTurnedIntoImages.count > 0
{
print("Selected image to preview:",assetsTurnedIntoImages[indexPath.row] )
destinationVC.transferedImageToPreview.image = assetsTurnedIntoImages[indexPath.item]
destinationVC.showInteractive()
}
}
}
}

You should update the requestImage method. requestImage method looks like async. So that request does not wait block to complete. If you update your block like this, it should work.
imageManager.requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .aspectFill, options: nil)
{ (image, info) in
DispatchQueue.main.async {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PhotoPostCVCell", for: indexPath) as! PhotoPostCVCell
cell.cellImage.contentMode = .scaleAspectFill
cell.cellImage.image = image!
self.assetsTurnedIntoImages.append(image!)
}
}

Related

Instead of deleting an array I want it to send the array to another UIViewController

I am trying to create a function that allows a user to select several videos and then select a button and it will send the selected videos to another array. I already have a similar function that handles deletions. I was really just trying to repurpose the code I already have for the deletion but everything I have tried has failed. I am a Swift newb but is there a way to do this or a better approach I should take?
var videos = [PHAsset]()
var dictionarySelectedIndexPath: [IndexPath: Bool] = [:]
#objc func didDeleteButtonClicked(_ sender: UIBarButtonItem) {
var deleteNeededIndexPaths: [IndexPath] = []
for (key, value) in dictionarySelectedIndexPath {
if value {
deleteNeededIndexPaths.append(key)
}
}
for i in deleteNeededIndexPaths.sorted(by: { $0.item > $1.item }) {
videos.remove(at: i.item)
}
collectionView.deleteItems(at: deleteNeededIndexPaths)
dictionarySelectedIndexPath.removeAll()
}
func getVideos() {
let assets = PHAsset.fetchAssets(with: PHAssetMediaType.video, options: nil)
assets.enumerateObjects({ (object, count, stop) in
self.videos.append(object)
})
self.videos.reverse()
self.collectionView.reloadData()
collectionView.delegate = self
collectionView.dataSource = self
let nib = UINib(nibName: "ItemCollectionViewCell", bundle: nil)
collectionView.register(nib, forCellWithReuseIdentifier: cellIdentifier)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "videoEditorSegueIdentifier" {
let otherVc = segue.destination as! VideoEditorVC
otherVc.videos = videos
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as! ItemCollectionViewCell
let asset = videos[indexPath.row]
let manager = PHImageManager.default()
if cell.tag != 0 {manager.cancelImageRequest(PHImageRequestID(cell.tag))}
cell.tag = Int(manager.requestImage(for: asset, targetSize: CGSize(width: 120.0, height: 120.0), contentMode: .aspectFill, options: nil) { (result, _) in cell.imageView?.image = result
})
return cell
}
As far as I can see, the issue you're having is because you're storing indexPath in a dictionary to remember what was selected and you're having difficulty translating that into actual data you're holding.
This would be far easier if you cut out the middle man and simply populated the array with actual selected objects in your didSelectItemAt method.
Something in the lines of:
var selectedVideos = [PHAsset]()
func videoFor(indexPath: IndexPath) -> PHAsset {
// return the video more or less the same as you do it in cellForItemAt:
}
func indexFor(video: PHAsset) -> Int? {
return selectedVideos.firstIndex(of: video)
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let video = videoFor(indexPath: IndexPath)
if selectedVideos.contains(video) {
selectedVideos.remove(at: index)
} else {
selectedVideos.append(video)
}
}

Image in Collection view cell is not updated when the image is downloaded asynchronously

The image in the collection view cell is not updated when the image is downloaded from the server. The image gets updated when the collection view is scrolled.
Every section of the table view has a collection view. And table view cell has datasource for the collection view.
extension OffersCell: UICollectionViewDataSource,UICollectionViewDelegate{
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return photoViewModel.photos.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "photoCell", for: indexPath)
(cell as! PhotoCell).imageView.contentMode = .scaleAspectFill
return cell
}
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
let photo = self.photoViewModel.photos[indexPath.row]
(cell as! PhotoCell).imageView.image = UIImage(named: "dummyImage")
ImageDownloadManager.shared.downloadImage(photo, indexPath: indexPath) { (image, imageIndexPath, error) in
if let indexPathNew = imageIndexPath, indexPathNew == indexPath {
DispatchQueue.main.async {
(cell as! PhotoCell).imageView.image = image
}
}
}
}
}
Please find the image downloader class :
typealias ImageDownloadHandler = (_ image: UIImage?, _ indexPath: IndexPath?, _ error: Error?) -> Void
final class ImageDownloadManager {
private var completionHandler: ImageDownloadHandler?
lazy var imageDownloadQueue: OperationQueue = {
var queue = OperationQueue()
queue.name = "imageDownloadQueue"
queue.qualityOfService = .userInteractive
return queue
}()
let imageCache = NSCache<NSString, UIImage>()
static let shared = ImageDownloadManager()
private init () {}
func downloadImage(_ photo: Photos, indexPath: IndexPath?, handler: #escaping ImageDownloadHandler) {
self.completionHandler = handler
guard let url = photo.getImageURL() else {
return
}
if let cachedImage = imageCache.object(forKey: photo.id as NSString) {
self.completionHandler?(cachedImage, indexPath, nil)
} else {
let operation = CustomOperation(url: url, indexPath: indexPath)
if indexPath == nil {
}
operation.queuePriority = .high
operation.downloadHandler = { (image, indexPath, error) in
if let newImage = image {
self.imageCache.setObject(newImage, forKey: photo.id as NSString)
}
self.completionHandler?(image, indexPath, error)
}
imageDownloadQueue.addOperation(operation)
}
}
func cancelAll() {
imageDownloadQueue.cancelAllOperations()
}
}
After you downloaded the image, you execute the instruction (cell as! PhotoCell).imageView.image = image on the main thread. But this does not redisplay your collectionView cell.
Also, collectionView:willDisplayCell:forItemAtIndexPath: will normally not be called. The docs say
The collection view calls this method before adding a cell to its
content.
It is however called, when you scroll in the cell, i.e. when it becomes visible. This is there reason why your image is displayed after the cell is scrolled in.
So my suggestion is:
After downloading the image, update your collectionView data source
so that collectionView:cellForItemAtIndexPath: can configure the cell
with the image.
Call reloadItems(at:) with an array that contains only the index path of the updated cell.
It depends on how you define the class CustomOperation, but the problem seems to be in the method downloadImage of ImageDownloadManager where in the next line you set self.completionHandler = handler. Note that ImageDownloadManager is a singleton. This means that every operation you start replaces completionHandler of the singleton object with the new completion (I bet only the last cell was refreshed). The solution consists of elimination the property completionHandler and replacing the operation download handler with this
operation.downloadHandler = { (image, indexPath, error) in
if let newImage = image {
self.imageCache.setObject(newImage, forKey: photo.id as NSString)
}
handler(image, indexPath, error)
}
Note that it calls the handler of the context and not the stored property of the download manager
Here is a full working example with all the class and struct definitions. Adapt it as needed.
typealias ImageDownloadHandler = (_ image: UIImage?, _ indexPath: IndexPath?, _ error: Error?) -> Void
enum ImageDownloadError: Error {
case badDataURL
}
class CustomOperation: Operation {
var downloadHandler: (UIImage?, IndexPath?, Error?) -> () = { _,_,_ in }
private let url: URL
private let indexPath: IndexPath?
init(url: URL, indexPath: IndexPath?) {
self.url = url
self.indexPath = indexPath
}
override func main() {
guard let imageData = try? Data(contentsOf: self.url) else {
self.downloadHandler(nil, self.indexPath, ImageDownloadError.badDataURL)
return
}
let image = UIImage(data: imageData)
self.downloadHandler(image, self.indexPath, nil)
}
}
final class ImageDownloadManager {
private var completionHandler: ImageDownloadHandler?
lazy var imageDownloadQueue: OperationQueue = {
var queue = OperationQueue()
queue.name = "imageDownloadQueue"
queue.qualityOfService = .userInteractive
return queue
}()
let imageCache = NSCache<NSString, UIImage>()
static let shared = ImageDownloadManager()
private init () {}
func downloadImage(_ photo: Photos, indexPath: IndexPath?, handler: #escaping ImageDownloadHandler) {
//self.completionHandler = handler
guard let url = photo.getImageURL() else {
return
}
if let cachedImage = imageCache.object(forKey: photo.id as NSString) {
//self.completionHandler?(cachedImage, indexPath, nil)
handler(cachedImage, indexPath, nil)
} else {
let operation = CustomOperation(url: url, indexPath: indexPath)
if indexPath == nil {
}
operation.queuePriority = .high
operation.downloadHandler = { (image, indexPath, error) in
if let newImage = image {
self.imageCache.setObject(newImage, forKey: photo.id as NSString)
}
//self.completionHandler?(image, indexPath, error)
handler(image, indexPath, error)
}
imageDownloadQueue.addOperation(operation)
}
}
func cancelAll() {
imageDownloadQueue.cancelAllOperations()
}
}
-------------------------------------------------------
struct Photos {
let id: String
let url: URL
func getImageURL() -> URL? {
return self.url
}
}
struct PhotoViewModel {
let photos: [Photos]
}
class PhotoCell: UICollectionViewCell {
#IBOutlet weak var imageView: UIImageView!
}
class ViewController: UIViewController {
#IBOutlet weak var collectionView: UICollectionView!
private let photoViewModel: PhotoViewModel = PhotoViewModel(
photos: [
Photos(
id: "kitty1",
url: URL(
string: "https://cdn.pixabay.com/photo/2019/06/18/11/23/cat-4282110_960_720.jpg"
)!
),
Photos(
id: "kitty2",
url: URL(
string: "https://cdn.pixabay.com/photo/2019/07/23/20/08/cat-4358536_960_720.jpg"
)!
),
Photos(
id: "kitty3",
url: URL(
string: "https://cdn.pixabay.com/photo/2016/09/28/13/15/kittens-1700474_960_720.jpg"
)!
)
]
)
override func viewDidLoad() {
super.viewDidLoad()
collectionView.dataSource = self
collectionView.delegate = self
collectionView.reloadData()
}
}
extension ViewController: UICollectionViewDataSource,UICollectionViewDelegate{
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return photoViewModel.photos.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "photoCell", for: indexPath)
(cell as! PhotoCell).imageView.contentMode = .scaleAspectFill
return cell
}
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
let photo = self.photoViewModel.photos[indexPath.row]
(cell as! PhotoCell).imageView.image = UIImage(named: "dummyImage")
ImageDownloadManager.shared.downloadImage(photo, indexPath: indexPath) { (image, imageIndexPath, error) in
if let indexPathNew = imageIndexPath, indexPathNew == indexPath {
DispatchQueue.main.async {
(cell as! PhotoCell).imageView.image = image
}
}
}
}
}
Yes, once image is downloaded is will not display unless collection view is scrolled as said by #Reinhard Männer
Instead you can go for the third-party SDKs(which fit your needs) for image downloading and caching in your app.
I will recommend to use Kingfisher SDK (developed in pure swift).
It is easy to use and integrate. it does lot of thing like async. downloading, caching(on memory or disk), built-in transition animation when setting images, etc. and it is popular too
For you'r problem it is one line code if you use Kingfisher SDK.
For eg.
To load image asynchronously you can use following in cellForRowAtItem: method.
let url = URL(string: "https://example.com/image.png")
imageView.kf.setImage(with: url)
What you all need to do is...
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "photoCell", for: indexPath) as! PhotoCell
cell.imageView.contentMode = .scaleAspectFill
//I'm assuming photo is URL(in string) of Photo. if 'photo' is URL type then you can pass it directly in 'setImage' method.
let photo = self.photoViewModel.photos[indexPath.row]
let imgUrl = URL(string: photo)
//It will download image asynchronously and cache it for later use. If the image is failed to downloaded due to some issue then "dummyImage" will be set in image view.
cell.imageView.kf.setImage(with: imgUrl, placeholder: UIImage(named: "dummyImage"))
return cell
}
Here you can remove cell willDisplay: method.

CollectionCell displays wrong image?

I have a TableView with cells, and one cell is holding a CollectionView.
Inside the CollectionView, I have cells with UIImageViews.
If I add new elements to the datasource while the CollectionView is visible then it works fine.
But if I scroll down in the TableView, add the new elements then scroll up, then even though it adds the new cells, they are displaying the wrong image.
Video: https://youtu.be/QwvMv2xaaAI
Code:
MainViewController(Not the whole)
func addNewPhotos(newPhotosArray: [Photo]){
var collectionViewInserts : [IndexPath] = []
for (i in 0...newPhotosArray.count) {
// I add the new photos to the datasource
PhotosStore.shared.photos.insert(newPhotosArray[i], at: 0)
// Then save the indexPath what needs to be inserted
collectionViewInserts.insert(IndexPath(row: i, section: 0), at: 0)
}
if let cell = self.tableView.cellForRow(at: IndexPath(row: 0, section: 0)) as? PhotosCell {
cell.photosCollectionView.performBatchUpdates({
cell.photosCollectionView.insertItems(at: collectionViewInserts)
}, completion: nil)
}
}
extension MainViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return PhotosStore.shared.photos.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PhotoCell", for: indexPath) as! PhotoCell
cell.photoImageView.downloadedFrom(link: (appSettings.url + "/resources/img/wp/prev/" + PhotosStore.shared.photos[indexPath.item].fileName))
return cell
}
}
PhotosCell:
import UIKit
class PhotosCell : UITableViewCell{
#IBOutlet weak var photosCollectionView : UICollectionView!
}
extension PhotosCell {
func setCollectionViewDataSourceDelegate<D: UICollectionViewDataSource & UICollectionViewDelegate>(_ dataSourceDelegate: D, forRow row: Int) {
// IF I PLACE A .reloadData() HERE, THEN IT WORKS BUT THEN THE CELL FLICKERS/JUMPS WHEN APPEARING ON SCREEN
let itemSize = 70
photosCollectionView.delegate = dataSourceDelegate
photosCollectionView.dataSource = dataSourceDelegate
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = UICollectionViewScrollDirection.horizontal
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0
layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
layout.itemSize = CGSize(width: itemSize, height: itemSize)
photosCollectionView.setCollectionViewLayout(layout, animated: true)
photosCollectionView.tag = row
photosCollectionView.setContentOffset(photosCollectionView.contentOffset, animated:false) // Stops collection view if it was scrolling.
photosCollectionView.reloadData()
}
var collectionViewOffset: CGFloat {
set { photosCollectionView.contentOffset.x = newValue }
get { return photosCollectionView.contentOffset.x }
}
}
What do I wrong? I do update the datasource correctly, I do perform batch updates on the collection view to insert the correct cells..
Updated details:
MainViewController:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// Photos on top
if indexPath.section == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "PhotosCell", for: indexPath) as! PhotosCell
cell.setCollectionViewDataSourceDelegate(self, forRow: indexPath.row)
cell.collectionViewOffset = storedPhotosCollectionViewOffset[indexPath.row] ?? 0
return cell
}
... other cells ...
}
Extension to download images: (I'm sure that's not the problem but just in case)
extension UIImageView {
func downloadedFrom(url: URL, contentMode mode: UIViewContentMode = .scaleAspectFit) {
image = nil
if let cachedImage = ImageCache.shared.loadCachedImage(url: url) {
image = cachedImage
return
}
contentMode = mode
URLSession.shared.dataTask(with: url) { data, response, error in
guard
let httpURLResponse = response as? HTTPURLResponse, httpURLResponse.statusCode == 200,
let mimeType = response?.mimeType, mimeType.hasPrefix("image"),
let data = data, error == nil,
let image = UIImage(data: data)
else {
DispatchQueue.main.async {
self.image = UIImage(named: "imageMissing")
}
return
}
DispatchQueue.main.async {
self.image = image
ImageCache.shared.cacheImage(image: image, url: url)
}
}.resume()
}
func downloadedFrom(link: String, contentMode mode: UIViewContentMode = .scaleAspectFit) {
guard let url = URL(string: link) else { return }
return downloadedFrom(url: url, contentMode: mode)
}
}
First in cellForRowAt
cell.photosCollectionView.reloadData()
return cell
Second you have to note that the image is downloaded ( consider a dummy image for the imageView or set a background to it ) every scroll so use SDWebImage

Swift: asynchronously loading and displaying photos

I'm struggling with the problem of displaying photo gallery from iPhone to collectionView.
Everything works fine if someone has 50 photos inside the gallery. The problem is when someone has thousands of photos, then the gallery is loading for 10 seconds, which is not good for my app. The same problem occurs when I'm loading images from Facebook. The app is waits until it downloads every photo then it displays. I'd like to display images one by one during the loading operation instead of waiting for it to load it all.
I know that I should use DispachQueue and I did but there is some bug that I don't see.
Here is the code I use to fetch images from iPhone gallery:
func grapPhotos() {
let imgManager = PHImageManager.default()
let requestOptions = PHImageRequestOptions()
requestOptions.isSynchronous = true
requestOptions.deliveryMode = .highQualityFormat
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
if let fetchResulat : PHFetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions) {
if fetchResulat.count > 0 {
for i in 0..<fetchResulat.count {
imgManager.requestImage(for: fetchResulat.object(at: i), targetSize: CGSize(width: 200, height:200), contentMode: PHImageContentMode.aspectFill, options: requestOptions, resultHandler: {
(image, eror) in
self.imageArray.append(image!)
DispatchQueue.main.async {
self.collectionView.reloadData()
}
})
}
} else {
print("You have no photos")
self.collectionView.reloadData()
}
}
}
And the code to display them in collectionView:
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return imageArray.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "galleryCell", for: indexPath) as! GalleryCollectionViewCell
let image = self.imageArray[indexPath.row]
DispatchQueue.main.async {
cell.gelleryImages.image = image
}
return cell
}
Probably the same problem is with my Facebook class so I will be very grateful for your help.
What you need is to add a fetchResult property to your collection view controller and fetch your image Assets inside viewDidLoad method.
var fetchResult: PHFetchResult<PHAsset> = PHFetchResult()
func fetchAssets() {
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
fetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions)
}
override func viewDidLoad() {
super.viewDidLoad()
fetchAssets()
}
Next step is extend UIImageView to request the image data asynchronously setting its image on completion.
extension UIImageView {
func fetchImage(asset: PHAsset, contentMode: PHImageContentMode, targetSize: CGSize, version: PHImageRequestOptionsVersion = .current, deliveryMode: PHImageRequestOptionsDeliveryMode = .opportunistic) {
let options = PHImageRequestOptions()
options.version = version
options.deliveryMode = deliveryMode
PHImageManager.default().requestImage(for: asset, targetSize: targetSize, contentMode: contentMode, options: options) { image, _ in
guard let image = image else { return }
switch contentMode {
case .aspectFill: self.contentMode = .scaleAspectFill
case .aspectFit: self.contentMode = .scaleAspectFit
#unknown default: fatalError()
}
self.image = image
}
}
}
Now you can fetch the images one at a time inside collection view cellForItemAt method:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! CollectionViewCell
let asset = fetchResult.object(at: indexPath.row)
cell.imageView.fetchImage(asset: asset, contentMode: .aspectFill, targetSize: cell.imageView.frame.size * UIScreen.main.scale )
return cell
}
Don't forget to return the fetchResult count for numberOfItemsInSection method.
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return fetchResult.count
}
class CollectionViewCell: UICollectionViewCell {
#IBOutlet weak var imageView: UIImageView!
}
extension CGSize {
static func *(lhs: CGSize, rhs: CGFloat) -> CGSize {
.init(width: lhs.width * rhs, height: lhs.height * rhs)
}
}
Have you looked into SDWebImage? It has built in cache support so that the device doesn't have to download images it has previously downloaded every single time. It also has the benefit of being very simple to implement!

Photos framework and reusable UICollectionViewCell

I have a UICollectionView to display photos from a device's album with the Photos framework. The photos are correctly displayed, but if I scroll fast (like when you tape at the top of the screen to go to the top of the collectionView), I have some photos which are not at the good indexPath. I just need to scroll a bit to put the bad photo out of the screen, and everything go back in place.
I clean the cell during prepareForReuse by canceling the current request.
I presume it's a problem with the asynchronous request of PHImageManager, but I don't know how to avoid this problem.
Here some code :
View Controller
extension AlbumDetailViewController : UICollectionViewDataSource {
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return photoList.count
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("PhotoCell", forIndexPath: indexPath) as! PhotoCollectionCell
cell.setImage(photoList.objectAtIndex(indexPath.row) as! PHAsset)
return cell
}
}
Custom CollectionViewCell
class PhotoCollectionCell: UICollectionViewCell {
#IBOutlet weak var imageView: UIImageView!
var requestId: PHImageRequestID!
let manager = PHImageManager.defaultManager()
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func prepareForReuse() {
self.imageView.image = nil
manager.cancelImageRequest(self.requestId)
}
func setImage(asset: PHAsset) {
let option = PHImageRequestOptions()
option.resizeMode = .Fast
option.deliveryMode = .HighQualityFormat
self.requestId = manager.requestImageForAsset(asset, targetSize: CGSize(width: self.frame.size.width * UIScreen.mainScreen().scale, height: self.frame.size.height * UIScreen.mainScreen().scale), contentMode: PHImageContentMode.Default, options: option, resultHandler: {(result, info)->Void in
self.imageView.image = result
})
}
}
Thank you
Before calling requestImageForAsset method, set cell.tag equal indexPath.item, and after when getting image in callback, check indexPath.item and cell.tag, if this is matched do that
self.imageView.image = result. For example
cell.tag = indexPath.item
if let asset = assets?[indexPath.item] {
let option = PHImageRequestOptions()
option.isSynchronous = false
option.resizeMode = .exact
option.isNetworkAccessAllowed = true
DispatchQueue.global(qos: .userInitiated).async {
manager.requestImage(for: asset, targetSize: Constant.targetSize, contentMode: .aspectFit, options: option, resultHandler: {(result, info)->Void in
DispatchQueue.main.async {
if cell.tag == indexPath.item {
cell.imageView.image = result
}
}
})
}
}
Hope, it helps you

Resources