I created a custom UIViewController and extended it to implement: UICollectionViewDataSource, UICollectionViewDelegate. In story board, I added UICollectionView and made reference from my controller to this UICollectionView (and setup the delegates for data source and collection view delegate).
I obviously implemented the minimal requirements for these 2 delegates. Now, since i sometimes asynchronously load images, I call cell.setNeedsLayout(), cell.layoutIfNeeded(), cell.setNeedsDisplay() methods after image download done (cell.imageView.image = UIImage(...)). I know all these methods are called. However, the images on the screen were not updated.
Did I miss something? Thank you!
Edit: Add Sample code - how update was called -
func collectionView(collectionView: UICollectionView,
cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! MyCollectionViewCell
....
let cellImage = ServiceFacade.sharedInstance.getImageFromCaching(image!.url)
if cellImage == nil {
// set image to default image.
cell.cellImage.image = UIImage(named: "dummy")
ServiceFacade.sharedInstance.getImage(image!.url, completion: { (dImage, dError) in
if dError != nil {
...
}
else if dImage != nil {
dispatch_async(dispatch_get_main_queue(),{
let thisCell = self.collectionView.dequeueReusableCellWithReuseIdentifier(self.reuseIdentifier, forIndexPath: indexPath) as! MyCollectionViewCell
thisCell.cellImage.image = dImage
thisCell.setNeedsLayout()
thisCell.setNeedsDisplay()
thisCell.layoutIfNeeded()
})
}
else{
// do nothing
}
})
}
else {
cell.cellImage.image = cellImage!
cell.setNeedsDisplay()
}
// Configure the cell
return cell
}
Chances are the UICollectionView has cached an intermediate representation of your cell so that it can scroll quickly.
The routine you should be calling to indicate that your cell needs to be updated, resized, and have it's visual representation redone is reloadItemsAtIndexPaths with a single index path representing your cell.
Related
I am trying to populate my UICollectionView with my data from CoreData database. The problem is that I want to show a photo in Collection Cell and using data to create UIImage - this task can take a while. With the current solution the images are loaded approx. after 3 seconds but all the other data is already shown in collection view.
How should I add the loading overlay and know when all the images are ready to hide it, or what is correct approach?
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: viewModel.reuseIdentifier, for: indexPath) as? LocationCollectionViewCell {
cell.name.text = viewModel.locations[indexPath.row].name
cell.unlocked.text = viewModel.locations[indexPath.row].unlocked ? "Unlocked" : "Locked"
if let data = viewModel.locations[indexPath.row].image {
DispatchQueue.global(qos: .background).async {
let image = UIImage(data: data)
DispatchQueue.main.async {
cell.image.image = self.viewModel.locations[indexPath.row].unlocked ? image : image?.grayscale()
}
}
} else {
cell.image.image = viewModel.locations[indexPath.row].unlocked ? UIImage(named: "noun_Akropolis_403786") : UIImage(named: "noun_Akropolis_403786")?.grayscale()
}
return cell
} else {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: viewModel.reuseIdentifier, for: indexPath)
return cell
}
}
So #peter you can try follow things:
You can add loader in cell and show/hide it when image is nil or not.
For that create an array where when any image is loaded just append that index there. Until array doesnt contain all index show loader in screen else hide loader.
For handling error you can simply use try catch. If image is not loading or it is falied then you shouldn provide dummy or placeholder image. So that loader will be removed at one time.
What I have:
I have a CollectionViewCell as .xib + .swift files.
Every cell gets their data from the database.
Inside every cell I have a like button.
What I want:
When I press the like button of a certain cell, I want to be able to read this data so I can change it and write the new data in the Database. So I want to change the like attribute of the dataset of a certain cell and save it in the DB
What I tried:
I have the indexPath of the cell but how can I read the data of the cell?
#IBAction func likeButton(_ sender: UIButton) {
var superview = self.superview as! UICollectionView
let buttonPosition:CGPoint = sender.convert(CGPoint.zero, to:superview)
let indexPath = superview.indexPathForItem(at: buttonPosition)
print(superview.cellForItem(at: indexPath!))
// Change picture
if sender.currentImage == UIImage(systemName: "heart.fill") {
sender.setImage(UIImage(systemName: "heart"), for: .normal)
} else {
sender.setImage(UIImage(systemName: "heart.fill"), for: .normal)
}
}
UICollectionViewDataSource
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: MyCollectionViewCell.identifier,
for: indexPath) as! MyCollectionViewCell
cell.backgroundColor = .white
let newShoeCell = shoesArray?.randomElement()
// Fill cells with data
cell.imageView.image = UIImage(named: newShoeCell!.imgName)
cell.shoeTitle.text = newShoeCell!.title
cell.price.text = String(newShoeCell!.price)
cell.ratingNumberLabel.text = String(newShoeCell!.meanRating)
cell.floatRatingView.rating = newShoeCell!.meanRating
if newShoeCell!.liked {
cell.likeButtonOutlet.setImage(UIImage(systemName: "heart.fill"), for: .normal)
} else {
cell.likeButtonOutlet.setImage(UIImage(systemName: "heart"), for: .normal)
}
return cell
}
You need to change your thinking. It is not the data "of a cell" A cell is a view object. It displays information from your data model to the user, and collects input from the user.
You asked "...how can I read the data of the cell?" Short answer: Don't. You should be saving changes into your data model as you go, so once you have an index path, you should use it to index into your data model and get the data from there.
You need to figure out which IndexPath the tapped button belongs to, and fetch the data for that IndexPath from your data model.
If you look at my answer on this thread I show an extension to UITableView that lets you figure out which IndexPath contains a button. Almost the exact same appraoch should work for collection views. The idea is simple:
In the button action, get the coordinates of the button's frame.
Ask the owning table/collection view to convert those coordinates to
an index path
Use that index path to fetch your data.
The only difference is that for collection views, the method you use to figure out which IndexPath the button maps to is indexPathForItem(at:) instead of indexPathForRow(at:)
I suggest you add a property to your cell
class MyCell: UITableViewCell {
var tapAction: (() -> Void)?
...
#IBAction func likeButton(_ sender: UIButton) {
tapAction?()
...
}
}
and set it in view controller
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
...
cell.tapAction = { [weak self] in
// call to database or whatever here and then reload cell
tableView.reloadRows(at: [indexPath], with: .automatic)
...
}
In general, cell should never care what its indexPath is, nor should it make calls to superview
I am making a multiple images upload process; I want user to preview their chose images and add some description for images. In here, I make a collection view inside a table view, I want the images show in collection view cell, but I don’t know what I should code to call the images that user picked.
This is image picker controller
#objc func btnClick() {
let picker = YPImagePicker(configuration: config)
picker.didFinishPicking { [unowned picker] items, _ in
if let photo = items.singlePhoto {
print(photo.fromCamera) // Image source (camera or library)
print(photo.image) // Final image selected by the user
print(photo.originalImage) // original image selected by the user, unfiltered
print(photo.modifiedImage) // Transformed image, can be nil
print(photo.exifMeta) // Print exif meta data of original image.
}
picker.dismiss(animated: true, completion: nil)
}
present(picker, animated: true, completion: nil)
picker.didFinishPicking { [unowned picker] items, cancelled in
for item in items {
switch item {
case .photo(let photo):
print(photo)
case .video(let video):
print(video)
}
}
picker.dismiss(animated: true, completion: nil)
}
}
performSegue(withIdentifier: "UploadTableViewController", sender: nil)
here is in the tableView controller
func collectionView(collectionView: UICollectionView,
cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("UploadCollectionViewCell",
forIndexPath: indexPath)
cell.image?.UIImageView =
return cell
}
I think you have to assign the image to your collection view cell in cellForItemAt:indexPath. So do something like:
cell.imageView.image = items[indexPath.row]
What else is missing is that once you have picked an image you have to update the collectionView. So just reload it by calling:
collectionView.reloadData()
Hope it helps.
In CellForRowAt, it is necessary to set the corresponding Image in UIImageView of cell.
cell.image = imageData[indexPath.item]
imageData is a group of Image data that User can select.
Then reload collectionView at any time.
collectionView.reloadData()
example code.
func collectionView(collectionView: UICollectionView,
cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("UploadCollectionViewCell",
forIndexPath: indexPath)
cell.image = imageData[indexPath.item]
// It works with either item or row. But for CollectionView, .item is preferable.
collectionView.reloadData()
return cell
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! LevelSelectCVC
cell.imageView?.image = imageArray[indexPath.row]
if indexPath.item >= stageProgress {
cell.imageView?.image = UIImage(named: "Lock Icon Grey")
} else {
cell.imageView?.image = imageArray[indexPath.item]
}
return cell
}
In this function I check the value of the integer stageProgress and add the same amount of images from an UIImage array to the collection view cells. The remaining of the cells adds a default UIImage. After returning from another view controller I have to check the stageProgress and add another UIImage from the array.
Does anyone know how I can do this? I have tried to do:
self.CollectionView.reloadData()
In both ViewDidAppear and PrepareForSegue
If I restart the game collectionView updates.
Calling ViewDidLoad() in ViewDidAppear resolved the issue.
I have a UITableView, I use the cellForRowAtIndexPath: function to download an image for a specific cell. The image download is performed asynchronously and finishes after a short amount of time and calls a completion block after finishing. I set the cell content to the downloaded image inside the completion block, but it doesn't get updated until after the user finishes scrolling. I know that the reason is because the code should be running in NSRunLoopCommonModes, but I don't know how to do that in this case, any help is appreciated.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let dequeued = self.tableView.dequeueReusableCellWithIdentifier(imageFileCellIdentifier)
let cell = dequeued as! ImageTableViewCell
source.FetchImage(imageLocation, completion: { (image) in
dispatch_async(dispatch_get_main_queue(), {
cell.previewImage.image = image
cell.previewImage.contentMode = .ScaleAspectFill
}
})
}
try this
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
let dequeued = self.tableView.dequeueReusableCellWithIdentifier(imageFileCellIdentifier)
let cell = dequeued as! ImageTableViewCell
source.FetchImage(imageLocation, completion: { (image) in
dispatch_async(dispatch_get_main_queue(), { () -> Void in
cell.previewImage.image = image
cell.previewImage.contentMode = .ScaleAspectFill
})
})
}
UI must be updated in main thread
You can use something like this to update imageView - coz asynchronous completion block may not call within main thread.
dispatch_async(dispatch_get_main_queue(), {
cell.previewImage.image = image
cell.previewImage.contentMode = .ScaleAspectFill
}
to update UIs
And also it is better if you check that the cell you are updating is the correct cell. Because when download completed(asynchronously) cell may be scrolled-out of the view and that cell may be allocated to another row. In that case you are setting the image to a wrong cell.
eg.
if cell.imageName == 'downloaded_image_name' {
// do update
}