I am using the collection view to show the gif's on the list.
Now facing the cell reusable issue while scrolling the cells up or down of collection view.
Like itemA is on first place in the list and itemB is on the second place in the list.
but when I scroll the data in the collection view. the places of items got misplaced. like some time itemA gone on 5th place or sometimes anywhere in the list.
i know i think this is the use with reusable cell, but don't know how to salve this.
Plss help.
Collection view cellForItemAt
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "GifCell", for: indexPath as IndexPath) as? GifCell else {
fatalError()
}
if gifArr.count > 0 {
let urlString = self.gifArr[indexPath.row]
let url = URL(string: urlString)!
DispatchQueue.global().async {
let imageData = try? Data(contentsOf: url)
let imageData3 = FLAnimatedImage(animatedGIFData: imageData) // this is the 3rd pary library to show the gifs on UIimageview's
DispatchQueue.main.async {
cell.imageView.animatedImage = imageData3
cell.textLabel.text = String(indexPath.row)
}
}
}
return cell
}
In GifCell you could implement prepareForReuse() method:
Performs any clean up necessary to prepare the view for use again.
override func prepareForReuse() {
super.prepareForReuse()
imageView.animatedImage = nil
textLabel.text = ""
}
Note:
at this point, each time cellForItemAt method gets called, the url will be reloaded, so later, you might want find a way to cache the images instead of keep reloading them.
First solution: You can cache data and every time check if there is, use your cache.
you can use this link, but replace UIImage with gift type!
or
try this, I did not test it
if let giftAny = UserDefaults.standard.value(forKey: "giftUrl") {
//cast giftAny to Data
// use cached gift
} else {
// cache gift
let giftData = try? Data(contentsOf: url)
UserDefaults.standard.setValue(giftData, forKeyPath: "giftUrl")
//use gift
}
Second Solution: Don't reuse cell
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = UICollectionViewCell(style: .default, reuseIdentifier:"Cell")
return cell
}
but in this case, if you have many cells, memory leak is unavoidable.
Related
I am trying to recreate this thing. I've created in Storyboard skeleton. Here's the idea of my code:
Fetch images from URL's array with help of the function getThumbnailFromImage
Add UIImage's with my thumbnails in array webImages
Add in ViewController reusable cell MyCollectionView
...
But here I am with this))) (Don't mind absence of Auto Layout). What am I doing wrong? I think that the problem is with reloadData() but I don't know where to put it.
ViewController:
//
// ViewController.swift
// youtube-clone
//
// Created by мас on 16.08.2022.
//
import Foundation
import UIKit
import YouTubePlayer
import AVFoundation
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
var url: [URL?] = [
URL(string: "https://www.youtube.com/watch?v=KhebpuFBD14"),
URL(string: "https://www.youtube.com/watch?v=UfNdNrRHpUw"),
URL(string: "https://www.youtube.com/watch?v=CX-BdDHW0Ho"),
URL(string: "https://www.youtube.com/watch?v=NIOMtSzfpck")
]
var webImages: [UIImage] = []
var currentPage: Int = 0
#IBOutlet var myPage: UIPageControl!
#IBOutlet weak var buttonInfo: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
setupLayout()
myPage.currentPage = 0
myPage.numberOfPages = webImages.count
}
// MARK: - Collection View Setup
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return webImages.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! MyCollectionCell
getThumbnailFromImage(url: url[indexPath.row]!, completion: { image in
self.webImages.append(image!)
})
cell.myWebImage.image = webImages[indexPath.row]
cell.myWebImage.layer.cornerRadius = 20
return cell
}
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
myPage.currentPage = indexPath.row
}
// MARK: - Layout Setup // IGNORE IT
func setupLayout() {
buttonInfo.layer.cornerRadius = 25
buttonInfo.imageView!.transform = CGAffineTransform(rotationAngle: 180 * .pi / 180)
self.navigationController?.navigationBar.largeTitleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
}
// MARK: - Videos Thumbnail Fetcher
func getThumbnailFromImage(url: URL, completion: #escaping ((_ image: UIImage?) -> Void)) {
DispatchQueue.global().async {
let asset = AVAsset(url: url)
let avAssetImageGenerator = AVAssetImageGenerator(asset: asset)
avAssetImageGenerator.appliesPreferredTrackTransform = true
let thumbnailTime = CMTimeMake(value: 7, timescale: 1)
do {
let cgThumbImage = try avAssetImageGenerator.copyCGImage(at: thumbnailTime, actualTime: nil)
let thumbImage = UIImage(cgImage: cgThumbImage)
DispatchQueue.main.async {
completion(thumbImage)
}
}
catch {
print(error.localizedDescription)
}
}
}
}
Reusable Cell AKA MyCollectionCell:
import UIKit
class MyCollectionCell: UICollectionViewCell {
#IBOutlet var myWebImage: UIImageView!
}
P.s.: YouTubePlayer is custom pod from GitHub, it's not currently used.
You do NOT have to use AVAssetImageGenerator, Simply you can use Youtube API to fetch the thumbnail images as .jpg image by video id,
and each YouTube video has four generated images.
https://img.youtube.com/vi/{id}/0.jpg
https://img.youtube.com/vi/{id}/1.jpg
https://img.youtube.com/vi/{id}/2.jpg
https://img.youtube.com/vi/{id}/3.jpg
Example
https://img.youtube.com/vi/KhebpuFBD14/0.jpg
And then it is preferred to use a third party to load this image as its displayed in a list, like https://github.com/SDWebImage/SDWebImage or https://github.com/onevcat/Kingfisher and you will NOT be worry about Concurrency or caching.
A couple of thoughts:
#matt is right in the comment - getThumbnailFromImage will likely not have called the completion block by the time cellForItemAt returns.
From what is visible in the code you posted, webImages.count will still be 0 when your collection view checks numberOfItemsInSection. If the number of items is 0, cellForItemAt may never get called so the call to getThumbnailFromImage wouldn't even be reached. (I'm not sure if the white box in your screenshot is part of a cell or another view element. If a cell is being displayed, I'm assuming you're populating webImages somewhere else before the collection view gets laid out).
One way you could work around these issues is by giving each cell a URL rather than a thumbnail. That way the cell can be displayed while the image is still loading. The cell could look something like this:
class MyCollectionCell: UICollectionViewCell {
#IBOutlet var myWebImage: UIImageView!
func configure(urlString: String) {
guard let self = self, let url = URL(string: urlString) else {
return
}
getThumbnailFromImage(url: url, completion: { [weak self] image in
self?.myWebImage.image = image
})
}
// Move `getThumbnailForImage` function to here, or give the cell a delegate to call back to the VC with if you don't want any networking in the view itself
}
The cellForItemAt function in the VC would need to be changed to something like this:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! MyCollectionCell
cell.configure(urlString: url[indexPath.row])
cell.myWebImage.layer.cornerRadius = 20 // This should probably live in the cell since the parent doesn't actually need to know about it!
return cell
}
An added benefit of this approach is that you're not referencing a separate array of images that could theoretically end up being in the wrong order if there's a mistake somewhere in the code. You could get rid of the webImages array entirely and use urls.count in numberOfItemsInSection instead - or eventually the number of elements returned from an API somewhere.
Side note - make sure you add [weak self] at the beginning of any closure that references self to avoid trying to access it after it's been deallocated! Currently the call to getThumbnailFromImage doesn't have that :)
Also, note that I changed to a guard statement for checking that the URL exists. This is much safer than force unwrapping a URL(string:) value, especially if you ever end up getting the strings from a dynamic source.
I'm making a music app recently, and I'd like to know how to pass data from CollectionView to TableView which has several sections. Here is the home page of my project, and what I want to do is when user tap the image, it will precent another ViewController with the information about the picture. I already know how to present a ViewController by clicking CollectionViewCell inside TableView by delegate, but only if there's only one section.
The thing is that I have 5 sections in this page, and I also have 5 different models for encoding the JSON from API. So how I show the pictures is to send the image urls(from 5 models) to TableViewCell in each section like this:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: HomeTableViewCell.identifier, for: indexPath) as? HomeTableViewCell else { return UITableViewCell() }
cell.delegate = self
switch homeSections[indexPath.section] {
case .newReleases:
if let newReleases = self.newReleases?.albums.items.map({$0.images[0].url}) {
cell.getPictures = newReleases
}
return cell
case .followSingers:
if let currentlyFollowing = self.currentlyFollowing?.artists.items.map({$0.images[0].url}) {
cell.getPictures = currentlyFollowing
cell.isCircle = true
}
return cell
case .catrgories:
if let categories = self.musicCategory?.playlists.items.map({$0.images[0].url}) {
cell.getPictures = categories
}
return cell
case .artists:
if let playlist = self.relatedArtists?.artists.map({$0.images[0].url}){
cell.getPictures = playlist
}
return cell
case .recentlyPlayed:
if let recentlyPlayed = self.recentlyPlayed?.items?.map({$0.track.album.images[0].url}) {
cell.getPictures = recentlyPlayed
}
return cell
}
}
However, when I want to pass the information which the user tap, there's nothing I can pass but indexPath. I've tried to declare the 5 different model types in TableViewCell, but I still don't know which section did the user tap. Does anyone can help? Thanks a lot!
Update:
To make the question more clearly, please refer to the information below.
In this page, I have a TableView and embed a CollectionView in the TableViewCell, and there's only one ImageView in the CollectionViewCell.
The "New Releases", "Currently Following" and "Categories" are the header of TableView. The "New Releases" is the first section, and the "Currently Following" is the second section, and so on.
Here is how I set cellForItem in CollectionView Delegate. It basically converts String to URL, and display the picture on the screen.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: HomeCollectionViewCell.identifier, for: indexPath) as? HomeCollectionViewCell else { return UICollectionViewCell() }
guard let url = URL(string: getPictures[indexPath.row]) else { return UICollectionViewCell() }
cell.myImageView.getImages(url: url)
if isCircle == true {
cell.myImageView.layer.cornerRadius = cell.myImageView.frame.width/2
}
return cell
}
When the user taps the image, it will only trigger the didSelectItemAt in CollectionView. And I can only pass indexPath.row so far.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
delegate?.sendIndexPath(index: indexPath.row)
}
I have a CollectionViewController in which I contact an API to download images based on a set of coordinates. I call this code in the cellForItemAt function at which time it updates the cell's images in realtime with images from Flickr. This works fine.
However, when scrolling up or down, it recalls this code and updates the cells again, when I'd prefer that it look at the existing cells, identify if they have been filled, and simply not run this code.
I have tried implementing logic before the networking code that checks to see if the imageView.images already exist in a local struct I assign them to, but that doesn't seem to work correctly.
Is there a simple method to tell cellForItemAt "for cells where you already have images, don't look for more"?
Here is my current code:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "imageCell", for: indexPath as IndexPath) as! CollectionViewCell
// Get images = using the URL
FlickrClient.sharedInstance().getImagesFromFlickr(latitude: selectedPin.lat, longitude: selectedPin.lon, page: pageCount) { (pin, error) in
if let pin = pin {
let url = pin.images[indexPath.item].imageURL
let data = try? Data(contentsOf: url)
performUIUpdatesOnMain {
cell.imageView.image = UIImage(data: data!)
cell.imageView.contentMode = .scaleAspectFill
}
}
}
return cell
}
Use SDwebImage libray for loading images from url.
https://github.com/rs/SDWebImage
Call Something like this on cell for row :
let url = pin.images[indexPath.item].imageURL
cell.imageView.sd_setImage(with: url, placeholderImage: UIImage(named: "placeholder.png"))
I've a collectionViewCell(dynamic) with images and labels (loading from JSON) inside a tableview. what i want is if i click on any Cell (contains image/folders) it should display detailedViewController with its content(also from JSON).
my mainViewController works fine. displays the DATA.
inside mainViewController i used didSelectItemAtIndexPath like this
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
println("USER TAPPED item# \(indexPath.item)")
let subFolderView:SubFolderViewController = self.storyboard?.instantiateViewControllerWithIdentifier("subFolder") as! SubFolderViewController
self.navigationController?.pushViewController(subFolderView, animated: true)
subFolderView.imageSelected = self.Data[indexPath.row].AbsolutePath
subFolderView.imageSelected = self.Data[indexPath.row].Name
subFolderView.imageSelected = self.Data[indexPath.row].Description
BUT it is returning empty view when clicked.
in my secondViewController i have
var imageSelected:String!
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
var cell:SubFolderCollectionViewCell? = collectionView.dequeueReusableCellWithReuseIdentifier("CollectionViewCell", forIndexPath: indexPath) as? SubFolderCollectionViewCell
cell?.subFolderDescription.text = self.Data[indexPath.row].Description
cell?.subFolderName.text = self.Data[indexPath.row].Name
let picUrl = self.Data[indexPath.row].AbsolutePath
let url = NSURL(string: picUrl)
if(url != nil) {
let data = NSData(contentsOfURL : url!)
if(data != nil) {
cell?.subImage?.image = UIImage(data: data!)
}
}
return cell!
}
Please any one guide me to proceed further......i am new to iOS development and prefer swift!!!!!!!
IN my storyBoadrd both viewControllers contains
View,
Tableview,
TableViewCell,
ContentView,
collectionView,
collectionViewCell.
inside that imageView and labels to display the data
Any help is appreciated,
Please let me know if u need more code............
Check your code..
you use didDeselectItemAtIndexPath instead of didSelectItemAtIndexPath method..
It's different tableview delegate method. change this and let me know if there is any issue.
I have an UICollectionView that makes a grid. Each cell to make the grid has a UIImage in it (created in IB).
I am using reuseable cells to keep the requests down.
How can I use this cell and the UIImage ? Is there someway of storing it in an array before it goes away ? I have created a tag but I don't if this will help ? If i created each cell manually then there will be around 100 #IBOutlets in my controller !! Here is my code to get the cells displayed..
Any ideas would be brilliant. I am trying to get the UIImage inside the cell, so I can hide it and also name it before the cell dequeues.
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("LetterCell", forIndexPath: indexPath) as UICollectionViewCell
cell.tag = indexPath.row
return cell
}
Images take up a surprising amount of memory. As a result, you generally do not want an architecture that requires you to hold all of images (or worse, cells) in memory at one time. You want your collection view cells to be reused and you want to retrieve the images from persistent storage in a just-in-time manner (aka, "lazy" image loading).
To minimize the memory footprint of your app, so your model would generally contain the minimal amount of information, for example just references to those images (such as filenames). Only load the images only when they're really needed by the UI.
For example, let's assume that the images were files in the “Application Support” folder of the device, then you might have an array of filenames (called imageNames in my example below), and you might do something like:
var imageNames = [String]() // this is populated elsewhere, perhaps `viewDidLoad`
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! CustomCell
let imageURL = try! FileManager.default
.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent(imageNames[indexPath.item])
cell.imageView.image = UIImage(contentsOfFile: imageURL.path)
return cell
}
If you really wanted to hold those images in memory (for example, for even smoother response time), you might use a NSCache, but make sure this cache empties itself upon receiving memory pressure. For example:
var imageCache = ImageCache()
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! CustomCell
let imageName = imageNames[indexPath.item]
if let image = imageCache[imageName] {
cell.imageView.image = image
} else {
let imageURL = try! FileManager.default
.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
.appendingPathComponent(imageName)
let image = UIImage(contentsOfFile: imageURL.path)
imageCache[imageName] = image
cell.imageView.image = image
}
return cell
}
Where
class ImageCache: NSCache<NSString, UIImage> {
var observer: NSObjectProtocol?
override init() {
super.init()
// empty queue upon memory pressure
observer = NotificationCenter.default.addObserver(forName: UIApplication.didReceiveMemoryWarningNotification, object: nil, queue: .main) { [unowned self] notification in
self.removeAllObjects()
}
}
deinit {
NotificationCenter.default.removeObserver(observer!)
}
subscript(key: String) -> UIImage? {
get { return object(forKey: key as NSString) }
set { setValue(newValue, forKey: key) }
}
}
There are other optimizations that one might consider, too. For example, if these images are large, you might make sure that you're loading the image view with images resized to something optimal for the collection view cell. But hopefully this illustrates some of the basic concepts when dealing with images in a UICollectionView.