In my project CollectionView in TableViewCell isn't display and I added
cell.collectionView.reloadData()
in my code. After I added, CollectionView displayed in TableViewCell, but
ScrollView isn't smooth. How to solve this problem. If someone have any experience or ideas help me. Thanks.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "homecell") as! HomeCategoryRowCell
let mainThemeList = mainHomeThemeTable[(indexPath as NSIndexPath).row]
DispatchQueue.main.async {
cell.categoryTitle.text = mainThemeList.main_name
cell.collectionView.reloadData()
}
return cell
}
CollectionView in my project,
extension HomeCategoryRowCell : UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
debugPrint("assetsTable count \(assetsTable.count)")
return assetsTable.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "videoCell", for: indexPath) as! HomeVideoCell
let list = assetsTable[(indexPath as NSIndexPath).row]
let url2 = NSURL(string:list.poster_image_url)
let data2 = NSData(contentsOf:url2! as URL)
// debugPrint(list.poster_image_url)
DispatchQueue.main.async(){
cell.movieTitle.text = list.name
cell.imageView.image = UIImage(data: data2! as Data)
}
return cell
}
}
Download Image Url Data in collectionView: UICollectionView, cellForItemAt indexPath: IndexPath delegate - ASYNCHRONOUSLY
Create a Class
First cache the image Data with respect to key Image URL
Why do we need to cache the data?
Because - While scrolling we don't need to download Image Every Time so Cache the already downloaded Image Data and return the cache data
I created a class name: AsyncImageLoader then I created a function which is a completion handler which returns the data after completion of downloading Image
class AsyncImageLoader {
static let cache = NSCache<NSString, NSData>()
class func image(for url: URL, completionHandler: #escaping(_ image: UIImage?) -> ()) {
DispatchQueue.global(qos: DispatchQoS.QoSClass.background).async {
if let data = self.cache.object(forKey: url.absoluteString as NSString) {
DispatchQueue.main.async { completionHandler(UIImage(data: data as Data)) }
return
}
guard let data = NSData(contentsOf: url) else {
DispatchQueue.main.async { completionHandler(nil) }
return
}
self.cache.setObject(data, forKey: url.absoluteString as NSString)
DispatchQueue.main.async { completionHandler(UIImage(data: data as Data)) }
}
}
}
How to use AsyncImageLoader Class?
See below How I used in collectionView: UICollectionView, cellForItemAt indexPath: IndexPath
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let yourCell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCollectionCell", for: indexPath) as! CustomCollectionCell
yourCell.url = URL(string: "\(item_ImageUrl)")
return yourCell
}
CustomCollectionCell
class CustomCollectionCell: UICollectionViewCell {
#IBOutlet weak var cellImageDisplayView: UIImageView!
var url: URL? {
didSet {
if let url = url {
cellImageDisplayView.image = nil
AsyncImageLoader.image(for: url, completionHandler: { (image) in
DispatchQueue.main.async {
if let img = image {
self.cellImageDisplayView.image = img
}else{
let dummyImage = UIImage(named: "img_u")
self.cellImageDisplayView.image = dummyImage
}
}
})
}else{
let dummyImage = UIImage(named: "img_u")
self.cellImageDisplayView.image = dummyImage
}
}
}
}
cell.layer.shouldRasterize = true
cell.layer.rasterizationScale = UIScreen.main.scale
After I added above code , my project become more smooth. I'll try the best and back with the best solution. Thanks
This is because you are directly getting data from url and then image from data in main thread. You should use image downloading library to download the image from url
Here is the good third party to download image from url asynchronously.
https://github.com/rs/SDWebImage
Try with this I am 100% sure it will solve your problem.
Thanks
Related
I've been working with Swift and iOS for several months now. I want to download a new image when it comes to CollectionView's penultimate item. I'm downloading, but the reloadData function is constantly redrawing and collectionview is top. How can I prevent it from going to the top?
private func getRandomPhoto() {
let url = "https://source.unsplash.com/random/400x800"
do{
var a = try Data(contentsOf: URL(string: url)!)
images.append(UIImage(data: a)!)
} catch {
print("err")
}
DispatchQueue.main.async {
self.collectionView.reloadData()
}
}
and here is my controller view codes
extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return images.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! ImageCollectionViewCell
cell.imageView.image = images[indexPath.row]
return cell
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
for cell in collectionView.visibleCells {
let indexPathh = collectionView.indexPath(for: cell)
if indexPathh?.row == images.count-1 {
let url = "https://source.unsplash.com/random/400x800"
getRandomPhoto()
}
}
}
}
I made a small video for the problem. I hope it's clear.
https://github.com/Egemenclr/StajCase/blob/master/Örnek/problem.gif
Don't reload the whole table, instead insert rows for newly downloaded images
insertItems(at indexPaths: [IndexPath])
suggestion: Don't download the same image only again and again. Download initially and cache it with NSCache. Try to fetch from cache when you need the image again.
I have two Viewcontroller A and B. A will download all the photo in collectionview then I can click on single cell to go to Viewcontroller B which is the tableview to show only this single image that passed from A.
But its working fine when first lunch the app but once I add new photo in the collectionview then I clicked this new photo to go to Viewcontroller B but B didn't show the correct photo showing some other photo. I print out the photo data postImg in viewcronller A and B the data is the same but B is not showing the correct photo. Why? When I rebuild the program it's working the issues seems only happened when I add new photo.
Here is code to load the pic from server
func loadPosts(){
let query = PFQuery(className: "posts")
query.whereKey("username", equalTo:PFUser.current()!.username!)
query.skip = self.picArray.count // skip the already loaded images
query.findObjectsInBackground { (objects, error) in
if error == nil {
if let objects = objects{
for object in objects{
self.collectionView?.performBatchUpdates({
let indexPath = IndexPath(row: self.uuidArray.count, section: 0)
self.uuidArray.append(object.value(forKey: "uuid") as! String)
self.picArray.append(object.value(forKey: "pic") as! PFFile)
self.collectionView?.insertItems(at: [indexPath])
}, completion: nil)
}
}
} else{
print(error!.localizedDescription)
}
}
}
Then var postImgData = [UIImage]() is the array used to save all the photos that downloaded from above function and this viable is defined at beginning of CLASS Viewcontroll A.
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return picArray.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
//define cell
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! pictureCell
picArray[indexPath.row].getDataInBackground { (data, error) in
if error == nil {
cell.picImg.image = UIImage(data: data!)
self.postImgData.append(UIImage(data: data!)!)
} else {
print(error!.localizedDescription)
}
}
return cell
}
Then i click the cell to go to viewcontroller B . postImg is defined at beginning of Viewcontroller B as global viable to get the image date that you click on Viewcontroller A cell.
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
postImg = postImgData[indexPath.row]
let post = storyboard?.instantiateViewController(withIdentifier: "postVC") as! postVC
self.navigationController?.pushViewController(post, animated: true)
}
Then in Viewcotnroller B to show the photo
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! postCell
cell.picImg.image = postImg
return cell
}
First:
picArray[indexPath.row].getDataInBackground { (data, error) in
if error == nil {
cell.picImg.image = UIImage(data: data!)
self.postImgData.append(UIImage(data: data!)!) <== WHY do you append array?
} else {
print(error!.localizedDescription)
}
you should probably init array beforehand and set data like self.postImgData[indexPath.row] = UIImage(data: data!)!
That how we ensure correct order of images
2
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let post = storyboard?.instantiateViewController(withIdentifier: "postVC") as! postVC
// Why dont use post.postImg = postImgData[indexPath.row]?
// Do not use Global vars, because you can change it from anywhere without noticing
self.navigationController?.pushViewController(post, animated: true)
}
So I suggest moving postImg var to controller B
I am currently having an issue with UICollectionViewCell and when it gets loaded.
Here is the link to the video to see it in action
Below is my code in viewDidLoad i call
retrieveUrls { (success) in
if success {
self.filterImageLabel(handler: { (success) in
if success {
if self.spinner != nil {
self.spinner.stopAnimating()
self.spinner.isHidden = true
}
self.collectionView.reloadData()
}
})
}
}
In retrieveUrls i am parsing the Url to retrieve image URL and for filterImageLabel i set up an photoUrlArray to use with collectionView indexpath
func filterImageLabel(handler: #escaping (_ status: Bool) -> ()) {
photoUrlArray = photoUrlArray.filter {$0.label == "Large Square"}
if photoUrlArray.count > 0 {
handler(true)
}
}
Methods for CollectionView
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return photoUrlArray.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "photoCell", for: indexPath) as? PhotoCell {
cell.updateCellImage(imageUrl: self.photoUrlArray[indexPath.row].source)
return cell
}
return PhotoCell()
}
And Finally in photocell class i am setting up the image
override func prepareForReuse() {
super.prepareForReuse()
photoImg.image = nil
}
func updateCellImage(imageUrl : String) {
Alamofire.request(imageUrl).responseImage(completionHandler: { (response) in
guard let image = response.result.value else {return}
self.photoImg.image = image
})
}
I have looked at various different thread on stack-overflow. however cannot seem to resolve the issue.
Any ideas would be helpful.
Method for CollectionView is as follows:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "photoCell", for: indexPath) as? PhotoCell {
// ********* ISSUE IS HERE *********
let imageurl = URL(string: self.photoUrlArray[indexPath.row].source)
cell.photoImg.af_setImage(withURL: imageurl!)
return cell
}
return PhotoCell()
}
I have a UIImageView and I want to append there images coming from a remote URL
Up to now it works fine with only one image.
I just set cell.related.image to the new image and I return always 1 to the collectionView
How can I switch to appending an array of images?
Is there a way to do the appending in a function?
Do I need always to do the appending inside collectionView ?
I want to do it inside an event listener in the future
var img_uls = ["url1" , "url2", "url3"]
func append_image(_ path: String, _ cell: RelatedCollectionViewCell) {
let url = URL(string: path)
DispatchQueue.global().async {
let data = try? Data(contentsOf: url!) //make sure your image in this url does exist, otherwise unwrap in a if let check / try-catch
DispatchQueue.main.async {
cell.related.image = UIImage(data: data!)
}
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return img_uls.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "RelatedCollectionViewCell", for: indexPath) as! RelatedCollectionViewCell
append_image(img_uls[indexPath.row], cell);
return cell
}
Just have the array of String which contains Url of your image and pass it as in numberOfItems Method like this
var img_uls = ["url1" , "url2", "url3"]
//array of strings containing urls
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return img_uls.count
}
Now you have to iterate your array img_uls, So that you can fetch Url from array and fetch the images and set in collectionView cell. So, do it like this
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "RelatedCollectionViewCell", for: indexPath) as! RelatedCollectionViewCell
append_image(img_uls[indexPath.row], cell);
// indexPath.row starts from (0) and goes to (img_uls.count - 1)
return cell
}
Now you want to append the image into images array
var images = NSMutableData()
func append_image(_ path: String, _ cell: RelatedCollectionViewCell) {
let url = URL(string: path)
DispatchQueue.global().async {
let data = try? Data(contentsOf: url!) //make sure your image in this url does exist, otherwise unwrap in a if let check / try-catch
DispatchQueue.main.async {
images.appendData(data)
cell.related.image = UIImage(data: data!)
}
}
}
Instead of using your own append method, and handling that append and all, you can use Kingfisher or Haenke. They are lightweight and moreover manages caching. And hence your scrolling will be fast.
Create a imageUrlArray variable of String which will hold the remote image URLs, then return the count of the array from your numberOfItemsInSection method. In collectionView:cellForItemAt function get the Url to display from the array using imageArray[indexPath.row] and pass the Url to the append_image(_ path: String, _ cell: RelatedCollectionViewCell) function. Better use third party image loading library like KingFisher or Nuke to better handle Asynchronous image downloading and caching.
class Example1: UIViewController, UICollectionViewDataSource {
var imageArray: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
}
func updateImages(_ images: [String]) {
self.imageArray = images
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return imageArray.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "RelatedCollectionViewCell", for: indexPath) as! RelatedCollectionViewCell
//append_image(imageArray[indexPath.row], cell)
//Using Kingfisher
let url = imageArray[indexPath.row]
cell.imageView.kf.setImage(with: url, placeholder: nil, options: [.transition(.fade(1))])
return cell
}
}
I have a horizontal scroll UICollectionView in a view controller. Whenever the view is scrolled, the images in the view disappear. I've tried to prepare for reuse with no success. I've posted the code below:
Collection View Cell
import UIKit
import SDWebImage
class StickerCell: UICollectionViewCell {
#IBOutlet weak var stickerImage: UIImageView!
override func prepareForReuse() {
super.prepareForReuse()
self.stickerImage.image = nil
}
func setImage(image: String) {
let url = NSURL(string: image)
self.stickerImage.contentMode = .ScaleAspectFit
self.stickerImage.clipsToBounds = true
self.stickerImage.center = self.center
self.stickerImage.sd_setImageWithURL(url)
}
}
View Controller
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.stickers.count
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell: StickerCell = collectionView.dequeueReusableCellWithReuseIdentifier("stickerCell", forIndexPath: indexPath) as! StickerCell
let sticker = stickers[indexPath.row]
if let image = sticker["images"]["medium"].string {
cell.setImage(image)
}
return cell
}
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
var cell = collectionView.cellForItemAtIndexPath(indexPath)
if cell?.selected == true {
let sticker = stickers[indexPath.row]
if let image = sticker["images"]["medium"].string {
let url = NSURL(string: image)
self.stickerView.contentMode = .ScaleAspectFit
self.stickerView.clipsToBounds = true
self.stickerView.sd_setImageWithURL(url)
}
}
}
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize {
return CGSizeMake(115, 115)
}
Try this..
-> Remove setImage method from strickerCell class.
-> Implement this
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell: StickerCell = collectionView.dequeueReusableCellWithReuseIdentifier("stickerCell", forIndexPath: indexPath) as! StickerCell
let sticker = stickers[indexPath.row]
if let image = sticker["images"]["medium"].string {
let url = NSURL(string: image)
cell.stickerImage.contentMode = .ScaleAspectFit
cell.stickerImage.clipsToBounds = true
cell.stickerImage.sd_setImageWithURL(url, placeholderImage:placeHolderImage, completed: { (image, error, cacheType, url) -> Void in
cell.stickerImage.image = image
})
}
return cell
}
I tied this the same way but the images seem to disappear over a period or time. Try setting some options on the Sd_setImage Call: for example:
self.userProfile.sd_setImage(with: URL(string: self.senderData.userProfileimg), placeholderImage: #imageLiteral(resourceName: "blankProfile"), options: [SDWebImageOptions.progressiveDownload, SDWebImageOptions.continueInBackground], completed: { (image, error, CachedType, url) in
self.userProfile.image = image
})