Why UICollectionView ReloadItems always calls CellForItemAt twice for each item? And how to prevent duplicate calls? How to prevent spending double resources for reloading of selected cells? ReloadData() calls CellForItemAt only once. But it's a waste of resources either to reload all rows and sections when only 2-3 items needed to be updated.
The code example may anything with UICollectiionView. For example:
import UIKit
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate {
#IBOutlet weak var collectionView: UICollectionView!
#IBAction func onTestButton(_ sender: Any) {
print("test button tapped")
collectionView.reloadItems(at: [IndexPath(item: 2, section: 0)])
}
let reuseIdentifier = "my cell"
var items = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]
override func viewDidLoad() {
collectionView.delegate = self
collectionView.dataSource = self
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.items.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath as IndexPath) as! MyCollectionViewCell
cell.myLabel.text = self.items[indexPath.row]
cell.backgroundColor = UIColor.cyan
print("cell for item at \(indexPath.row)")
return cell
}
}
When tapping "test button", console output will be:
test button tapped
cell for item at 2
cell for item at 2
But I need to reload "item at 2" only once, and I don't want to reload all items using reloadData().
You should not make any assumptions about how often the system will call your collectionView(_:cellForItemAt:) method. Just build and return cells when it asks you to. If it sometimes asks you for the same cell twice, just give it the same cell twice.
Collection views and table views ask your to return cells frequently, as the user scrolls. Your collectionView(_:cellForItemAt:) method (or the equivalent table view method) needs to be fast. It shouldn't do network requests, or even decompress JPEG or compressed PNG images from disk unless you cache the results.
Related
I'm trying to show some images and hide others using ".isHidden" in my CollectionView. But when I scroll down or reload the collectionView they either get reordered incorrectly or hidden entirely.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ReadBookCell", for: indexPath) as! ReadBookCell
let item = readBookArray[indexPath.item]
for star in cell.starImgOutletCollection {
if star.tag <= item.starRating {
star.isHidden = false
} else {
star.isHidden = true
}
}
return cell
}
Edit: Here is my prepareForReuse
override func prepareForReuse() {
super.prepareForReuse()
for star in starImgOutletCollection {
star.isHidden = true
}
}
Assuming your "stars" are 5 image views in a stack view like this:
And, assuming your starRating will be between 0 and 5 (Zero being no rating yet)...
In your cell class, create a reference to the stack view - since your question mentions starImgOutletCollection I'm assuming you are using #IBOutlet (that is, not creating your views via code), so:
#IBOutlet var starsStackView: UIStackView!
Then, still in your cell class, add this func:
func updateStars(_ starRating: Int) {
for i in 0..<starsStackView.arrangedSubviews.count {
starsStackView.arrangedSubviews[i].isHidden = i >= starRating
}
}
Now, in cellForRowAt
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ReadBookCell", for: indexPath) as! ReadBookCell
let item = readBookArray[indexPath.item]
cell.updateStars(item.starRating)
// do the other stuff to set labels, images, etc
// in the cell
return cell
}
You no longer need the outlet collection for the "star" image views, and you no longer need to implement prepareForReuse().
I've created an collection view with carName's there are 5 of them, after clicking for example Mercedes(one of the collection view's cell) I want to set label text its own values:carModel ( carName and carModel are both same struct properties ) in Table view which is already created by me, but I cant access carModel array
I tried for loop but it returns an error
for i in cars.carModel {
lbl.text = cars.carModel[i]
}
any solution will be appericated
// data source file
struct Cars {
let carName:String
let carModel:[String]
}
let cars:[Cars] = [
Cars(carName: "Mercedes", carModel: ["S Class","A Class", "B Class"]),
Cars(carName: "BMW", carModel: ["X5","X6","X7"]),
Cars(carName: "Ford", carModel: ["Fuison","Focus","Mustang"]),
Cars(carName: "Toyota", carModel: ["Camry", "Corolla"]),
Cars(carName: "Hyundai", carModel: ["Elantra"])
]
// table view cell file
class TableViewCell: UITableViewCell {
#IBOutlet var lbl: UILabel!
func configure(with cars:Cars){
lbl.text = cars.carName
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
}
}
// mainviewcontroller
class ViewController: UIViewController {
#IBOutlet weak var collectionView: UICollectionView!
//#IBOutlet weak var tableView
override func viewDidLoad() {
super.viewDidLoad()
collectionView.dataSource = self
collectionView.delegate = self
}
}
extension ViewController: UICollectionViewDataSource{
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return cars.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell", for: indexPath) as? CollectionViewCell
cell?.setup(with: cars[indexPath.row])
return cell!
}
}
extension ViewController:UICollectionViewDelegate,UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 5
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let vc = storyboard?.instantiateViewController(identifier: "TableViewController") as? TableViewController
self.navigationController?.pushViewController(vc!, animated: true)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell:TableViewCell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCell
cell.configure(with: cars[indexPath.row])
return cell
}
}
Here's my simulator:
img1
img2
what I want:expected result img
When you defined your UITableViewDataSource you set the number of rows in function method to return a constant (5). In cellForRowAt function you setup the cell with a Cars object.
I think you are mixing what data should be use for table view's datasource.
For collection view data source you have to:
Number of rows in section function should return: cars.count
Cell for item at function must configure CollectionViewCell with a Cars instance
I think all you are missing is saving which cell is being selected in collection view to use this to supply correct data to table view.
In collection view delegate when user taps on a cell you can either save the selected indexPath or Cars instance, this will be use by table view delegate. Let's call it selectedCar.
Then for table view data source you have to:
Number of rows in section function should return: selectedCar.carModel.count
Cell for item at function must configure TableViewCell with a CarModel instance (which is a String)
I created a small project in Github as a sample, is working as expected. Check it out. Hope this helps you. Sample video in Youtube.
Note: Setting datasource and delegate for tableView or collectionView in ViewController is not the best way to organize code but for sample code is ok.
I am using swift. I'm trying to create a collectionView segue to lead to a new viewController.
I have a series of (lets say 8) different images and labels as an array within the collectionView, and the when selected, I want the user to be sent to another view controller (with 8 different possibilities - one for each cell). I have been able to get the app to build, but the behaviour from selecting a cell is wrong.
The first cell that is selected has no response, then the next cell initiates a segue - but to the previously selected one! Each time a different cell is selected, it segues to the previous selected cell. Can anyone help me correct this error?
I have used performSegue and pushViewController separately, following different tutorials on youtube, but each resolves to the same issue.
The various view controllers to segue to have been allocated their own storyboardID in the main.storyboard file. The initial view controller is embedded within a navigation controller, and each new veiwcontroller (to segue to) has been connected to the collection view of the main view controller.
import UIKit
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
#IBOutlet var CollectionView: UICollectionView!
// created an array for labels
let NamesForSectionTitles = ["Overview","Canopy trees","Mid-story trees","Understory plants","Birds","Mammals","Ferns","Butterflys"]
// created list of images to be loaded in the collectionview
let sectionIconImages: [UIImage] = [
UIImage(named: "IMG_8750_landscape_night")!,
UIImage(named: "IMG_8789_Trees")!,
UIImage(named: "IMG_2185_Tree_Olearia")!,
UIImage(named: "_MG_9528_Herb_Flower")!,
UIImage(named: "IMG_3654-")!,
UIImage(named: "IMG_9892-2")!,
UIImage(named: "IMG_9496_Ferns_crozier")!,
UIImage(named: "IMG_7707_Butterfly")!,
]
override func viewDidLoad() {
super.viewDidLoad()
// load the data sources and delegate
CollectionView.dataSource = self
CollectionView.delegate = self
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return NamesForSectionTitles.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) ->
UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CollectionViewCell
cell.SectionTitle.text = NamesForSectionTitles[indexPath.row]
cell.sectionImages.image = sectionIconImages[indexPath.row]
return cell
}
// tells the collection view that upon selecting a cell, show the next UIView controller, as suggested in storyboard name (main.storyboard property) - tutoral from https://www.youtube.com/watch?v=pwZCksvXGRw&list=PLPUDRZDcNNsMdyfZVw4CJDT1Wu8cYx_6E&index=7&t=0s
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
performSegue(withIdentifier: NamesForSectionTitles[indexPath.row], sender: self)
}
}
The viewer should see an image view with a label, that when selected takes them to a new collection view.
Can't Comment so posting an answer
I think
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) is causing the issue.
Please try changing that line to:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
I have a CollectionView setup and i have a button inside that increases the cell height by a fixed amount. However when it does this is overlapps with the cell below meaning the cell below doesnt move down to make space.
Ive been trying to figure this out for days and i cant seem to get it working.
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
var colors = [UIColor.red,UIColor.blue]
#IBOutlet weak var col: UICollectionView!
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 2
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! cellCollectionViewCell
cell.frame.size.height = CGFloat(317 + cell.button.tag*45)
cell.backgroundColor = colors[indexPath.row]
return cell
}
#IBAction func increaseHeight(_ sender: Any) {
col.reloadData()
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
}
class cellCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var butt: UIButton!
#IBAction func incHeight(_ sender: UIButton) {
sender.tag = sender.tag + 1
}
}
Editing the frame of the cell directly is probably not the best approach. Instead override collectionView sizeForItemAtIndexPath, and specify the height change there. This should allow the collection view to re-layout correctly with the new cell height.
You can see other questions like this one for more details
The code is written in Swift. I'm building a social app where the user can make posts. I'm using Firebase as a backend (database, storage). So, I have a UICollectionView that gets all the photos from the photo library of the device and populate the collection view using a custom cell. In the same View controller, I have another custom cell that the user can use to take a photo and use it to make a post. To make it clearer:
If the user decides to take a photo, when they click on "Use photo" they need to be presented to a new view controller that should display the photo they just took along with other options (such as title, description and tags using UITextFields & UITextView).
If the user decides to select multiple photos from their own library, I have to somehow mark those photos/cells (i.e. using a button for a checkmark), add the selected photos to an array (with some limit, maybe 10 photos top). When they click "Next" button, the array needs to be sent to the New Post View Controller where all the images should be dynamically displayed maybe using a horizontal UICollectionView (?!) (with an option to remove an image if it was selected by accident) and again, as above, have the opportunity to add title, description, etc. Now, I cannot figure out how to do any of that.
I looked for a solution, but I'm kind of stuck on this for couple days now, so help is very much welcome!
Here's what I have in the Collection View controller (PS: I didn't include the part with the function that gets the images from the Photos)
import UIKit
import Photos
class PrePhotoPostVC: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UINavigationControllerDelegate, UIImagePickerControllerDelegate, UICollectionViewDelegateFlowLayout {
#IBOutlet weak var nextButton: UIBarButtonItem!
var photosLibraryArray = [UIImage]()
#IBOutlet weak var collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
checkPhotoLibraryPermission()
setupCollectionViewDelegates()
}
#IBAction func cancelButtonPressed (_ sender: UIBarButtonItem) {
dismiss(animated: true, completion: nil)
}
#IBAction func nextButtonPressed (_ sender: UIBarButtonItem) {
nextButton.isEnabled = false
}
#IBAction func takeAphotoButtonPressed (_ sender: UIButton) {
// Camera Autorization
AVCaptureDevice.requestAccess(forMediaType: AVMediaTypeVideo) { response in
if response {
if UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.camera) {
let imagePicker = UIImagePickerController()
imagePicker.delegate = self
imagePicker.sourceType = UIImagePickerControllerSourceType.camera;
imagePicker.allowsEditing = false
self.present(imagePicker, animated: true, completion: nil)
}
else {
print("Camera isn't available in similator")
}
}
else {
print("unautorized")
}
}
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 2
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
if section == 0 {
return 1
} else {
return photosLibraryArray.count
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if indexPath.section == 0 {
let cellCamera = collectionView.dequeueReusableCell(withReuseIdentifier: cellPrePostCameraCell, for: indexPath)
return cellCamera
}
else {
let cellPhotoLibrary = collectionView.dequeueReusableCell(withReuseIdentifier: cellPrePostPhotoLibrary, for: indexPath) as! PrePhotoPostPhotoLIbraryCell
cellPhotoLibrary.awakeFromNib()
cellPhotoLibrary.photoLibraryImage.image = photosLibraryArray[indexPath.row]
return cellPhotoLibrary
}
}
}
A screenshot of what this UICollectionView looks like:
Here's my code from the Photo Library Cell:
import UIKit
class PrePhotoPostPhotoLIbraryCell: UICollectionViewCell {
// MARK: Outlets
#IBOutlet weak var photoLibraryImage: UIImageView!
// var selectedPhotos = [UIImageView]()
#IBAction func selectedButtonPressed(_ sender: UIButton) {
self.layer.borderWidth = 3.0
self.layer.borderColor = isSelected ? UIColor.blue.cgColor : UIColor.clear.cgColor
}
override func awakeFromNib() {
photoLibraryImage.clipsToBounds = true
photoLibraryImage.contentMode = .scaleAspectFill
photoLibraryImage.layer.borderColor = UIColor.clear.cgColor
photoLibraryImage.layer.borderWidth = 1
photoLibraryImage.layer.cornerRadius = 5
}
}
First of all declare an array of mutable type that will store the selected cells item in it.
var _selectedCells : NSMutableArray = []
then in your viewDidLoad function add below code.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
//this will allow multiple selection on uicollectionviewcell
CollectionView.allowsMultipleSelection=true //CollectionView is your CollectionView outlet
}
Then, Implement delegate functions of collectionview for selecting and deselecting cells
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath){
//add the selected cell contents to _selectedCells arr when cell is selected
_selectedCells.add(indexPath)
collectionView.reloadItems(at: [indexPath])
}
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
//remove the selected cell contents from _selectedCells arr when cell is De-Selected
_selectedCells.remove(indexPath)
collectionView.reloadItems(at: [indexPath])
}
I'd suggest saving the NSIndexPath of the selected item in an array, and then using that for the basis of comparison in the delegate function cellForItemAt indexPath.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "YOUR_CELL_Identifier", for: indexPath as IndexPath)
//add your tick mark image to the cell in your storyboard or xib file.
let tickImage = cell.viewWithTag(YOUR_IMAGE_TAG_HERE) as? UIImageView
//Show tickImage if the cell is selected and hide tickImage if cell is NotSelected/deSelected.or whatever action you want to perform in case of selection and deselection of cell.
if _selectedCells.contains(indexPath) {
cell.isSelected=true
collectionView.selectItem(at: indexPath, animated: true, scrollPosition: UICollectionViewScrollPosition.top)
tickImage?.isHidden=false
}
else{
cell.isSelected=false
tickImage?.isHidden=true
}
return cell
}
In Order to send items to next controller, get all the items from selected indexpaths.