Swift - UICollectionView reloadItems takes too long - swift

I want to reload only single cell after user select the contact (method didSelect contact). If the number of item in cell getting bigger, the process is getting slower. I also try to implement in DispatchQueue.main.async { code } and it still take some time to load.
cellForItemAt and noOfItemInSection (AddCell) - Total cell 4
var indexPaths = [IndexPath]()
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
indexPaths.append(indexPath)
if indexPath.item == 1 {
let addCell = collectionView.dequeueReusableCell(withReuseIdentifier: CellId, for: indexPath) as! AddCell
addCell.btnAdd.addTarget(self, action: #selector(handleOpenContact), for: .touchUpInside)
addCell.invitees = invitees
return addCell
}
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 4
}
cellForItemAt and noOfItemInSection (SubAddCell)
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: subAddId, for: indexPath) as! SubAddCell
cell.delegate = self
cell.invitee = invitees[indexPath.item]
return cell
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return invitees.count
}
didSelect (ContactPicker)
func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) {
let invitee = Invitee()
invitee.name = contact.givenName
if contact.isKeyAvailable(CNContactPhoneNumbersKey) {
for phoneNumber: CNLabeledValue in contact.phoneNumbers {
let a = phoneNumber.value
print("\(a.stringValue)")
invitee.phoneNo = a.stringValue
}
}
self.invitees.append(invitee)
for index in self.indexPaths {
if index == [0, 0] {
print(index)
self.collectionView?.reloadItems(at: [index])
}
}
}
I have obtain the value of indexPaths at cellForItemAt method. If the invitee contains 5 items, the print(index) will print 19 values. I dont know why. Below is the image.
Design of the UI
How to optimize the reloadItems?
Any help is really appreciated. Many thanks.

I think you have this indexPaths.append(indexPath) in cellForItemAt so that why the indexPaths can increase after reload data. You might use NSSet instead of NSArray to make sure your objects are unique.
And with this: cell.invitee = invitees[indexPath.item], did you implement didSet for invitee?
If you did, I think need to prepare data in background queue first. E.g.
DispatchQueue(label: "queue").async {
let image = <# process image here #>
let name = <# process name here #>
DispatchQueue.main.async {
self.imageView.image = image
self.label.text = name
}
}
Hope my tips could help you!

Collectionviews are the most powerful UI tool of iOS, but if done wrong, or if misused (which happens, don't worry), they can be worse for your UX. . To view how this stuff is affected read up on XCode Tools and how to use them to debug your UI performance.
Important note to consider:
frame-by-frame operations (ie, loading your collectionviewcell, expanded, or unexpanded) should take no more than 17 ms to execute for smooth scrolling.
UICollectionView performance - _updateVisibleCellsNow
So you need to make your cells as simple and efficient as possible. That's the best place to start, especially if your expandable cells themselves contain complex subviews like collectionviews.

Related

Make the first cell automatically selected when VC opens is not working

I have a ViewController that have a collectionView and I managed to make it selectable and all but the problem is that I have a checkmark image that stays in the first cell when the VC opens but in fact the cell is not selected at all and still the checkmark is there!
Code of the VC:
var selected = IndexPath(item: 0, section: 0)
var properties = connectedProperties(StatusCode: 0)
var propertiesNew = connectedProperties(StatusCode: 0)
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return properties.Result?.count ?? 0
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "dashboardCollectionViewCell", for: indexPath) as? dashboardCollectionViewCell else { return UICollectionViewCell() }
let currentPropertie = properties.Result?[indexPath.row]
cell.checkMarkButton.isHidden = !(indexPath == selected)
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let selectedCell = properties.Result?[indexPath.row]
changeCustomerKey.DefaultsKeys.keyTwo = indexPath.row
changeCustomerKey.DefaultsKeys.keyThree = selectedCell!.id!
let previous = selected.dropLast()
selected = indexPath
collectionView.reloadItems(at: [previous, selected])
}
If you want to tell your collection view to select a specific cell, you need to call the UICollectionView method selectItem(at:animated:scrollPosition:).
The "tricky bit" is that you can't call that until the collection view has finished populating itself (by calling your data source methods) and the first cell has been added to the collection view.
You might need to resort to something a bit hacky like adding an "initialDisplay" bool property who's value starts as true.
In your data source method that returns cells, check if initialDisplay==true and the requested indexPath is (0,0). If so, set initialDisplay=false, and fire a one-shot timer with a short delay. In the timer's closure, call selectItem(at:animated:scrollPosition:). The timer delay will return control to the event loop and give the system time to add the cell to the collection view.
There might be a better way to do this, but I can't think of it offhand, since you can't be sure when you will be asked to return your cell at IndexPath (0,0)

Showing/Hiding images in CollectionView Cell

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().

UICollectionViewCell is populated with previous cells data before new the cell's data loads, need to implement PrepareForReuse()

I have a problem with my UICollectionView, The problem is that if you scroll fast enough (not even that fast) the data that was in the cell before the reuse gets displayed for a second or two before the new data gets displayed.
This is the video of it happening for more context (youtube video link): https://youtu.be/I63hBuxBGI0
This is the code inside of the cellForItemAt:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if let post = feedElements[indexPath.row] as? FeedPost {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! feedCollectionViewCell
cell.delegate = self
cell.post = post
cell.commentButton.tag = indexPath.row
// assigning all of the data
cell.captionLabel.text = post.caption
cell.locationLabel.text = post.location
cell.timeAgoLabel.text = post.timeAgoString
cell.setUpElements()
cell.prepareForReuse()
return cell
}
}
The code for inside of the numberOfRowsInSection:
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return feedElements.count
}
I'm pretty sure what I need to implement is prepareForReuse() but I'm not sure exactly how to do that and couldn't find anything helpful online.
Thanks a lot!
You need to add prepareForReuse in feedCollectionViewCell class like this
class feedCollectionViewCell: UITableViewCell {
override func prepareForReuse() {
super.prepareForReuse()
// Reset your values here as you want ...
self.post = nil
self.commentButton.tag = 0
// assigning all of the data
self.captionLabel.text = nil
self.locationLabel.text = nil
self.timeAgoLabel.text = nil
}
}

How to programmatically set up a dynamic number of collection views in Swift

I have a list of many Decks, which is mainly has a name and a list of Cards. The below shows the class structure.
class Deck{
var id:String
var name:String
var cards:[Card]
init(id:String, name:String, cards:[Card]) {
self.id = id
self.name = name
self.cards = cards
}
}
class Card {
var id:String
var name:String
init(id:String, name:String){
self.id = id
self.name = name
}
}
I need to be able to create a single UICollectionView for each deck, where each cell should represent a button with the single card's name, and I need to be able to identify which cell's button is tapped. This means that I need to allow both dynamic number of UICollectionView(list of decks) and a dynamic number of UICollectionViewCell(list of cards). Please note that I already understand how to create the cells dynamically and identify which cell's buttons are tapped.
Overall, the main issue lies in the fact that I won't know how many decks and hence how many UICollectionViews I have to create. But, since the structure across all UICollectionViews are the same but populated with different values (i.e number of cells, cell's label title has different values), I am wondering what is the way to dynamically create UICollectionViews (NOT UICollectionViewCells) and populate them with decks from a list like decks:[Deck]
Thanks a bunch! The image at the bottom shows roughly what I would like to do.
It's still not clear if you really need a collection of UICollectionView objects or you could do with multiple sections in the same collection view. Anyhow, if you do like multiple collection views just make a collection or table view displaying decks list and put collection view for the deck in the cell.
class DeckCell: UICollectionViewCell { // or class DeckCell: UITableViewCell
var deck: Deck = .empty
#IBOutlet weak var collectionView: UICollectionView!
}
extension DeckCell: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return self.deck.cards.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CardCell", for: indexPath)
cell.card = self.deck.cards[indexPath.row]
return cell
}
}
Here is simple solution for you.
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
//number of items in Decks Array will be its count
if collectionView == dockCollectionView {
return Decks.count //It will tell the collection view how much cell will be there
}
if collectionView == cardsCollectionView {
return Cards.count
}
}
//Setup your resuable cell here
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
//Setup for Dock CollectionView
if collectionView == dockCollectionView {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Your Dock Cell Identifier Name", for: indexPath)
// This will return a each item name from the item list and display in to a cell
cell.textLabel.text = Decks[indexPath.row].name
return cell
}
//Setup for Cards CollectionView
if collectionView == cardsCollectionView {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Your Cards Cell Identifier Name", for: indexPath)
// This will return a each item name from the item list and display in to a cell
cell.textLabel.text = Cards[indexPath.row].name
return cell
}
}
//Now get to know which cell is tapped
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if collectionView == dockCollectionView {
print(Decks[indexPath.row].name)
}
if collectionView == cardsCollectionView {
print(Cards[indexPath.row].name)
}
// Here you write a code when user select a particular cell
}
You already have an solution you have a list in which you have a items so you have to implement dynamically so you just need to count a number of item's in a list.
So for do that you use items.count in to func:-
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return items.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Your Cell Identifier Name", for: indexPath)
// This will return a each item name from the item list and display in to a cell
cell.textLabel.text = item[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
// Here you write a code when user select a particular cell
}

Wrong images loaded after scrolling in collectionView swift 3

I am having issues with my collectionView in Swift 3. I have found some tips on Stackoverflow, tried them out, but alas to no avail. Many mentioned to use the 'prepareReuse' method, but I could not get those to work in my code. After Scrolling down, and back up again, the images have changed. All the images are letters of the alphabet. So A,B,C are the first images to appear at the top of the view. If you scroll down, and back up, some random other letters have taken their place. My entire code is as follows:
import UIKit
class colViewController2: UIViewController, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource {
var imageData: [String] = [String]()
var imageCounter: Int = 0
var userHasHieroglyph: NSArray = ["","","C","D","E","F","G","H","I","J","K","L","M","N","O","","Q","R","S","T","U","V","W","X","Y","Z"]
override func viewDidLoad() {
super.viewDidLoad()
imageData = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"]
for b in imageData {
if userHasHieroglyph.contains(b) {
let newHieroglyph = b.lowercased()
imageData[imageData.index(of: b)!] = "h-"+newHieroglyph
}
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellid", for: indexPath) as! MyImageCell
cell.backgroundColor = UIColor.white
var currImage:String = ""
currImage = self.imageData[self.imageCounter]
self.imageCounter += 1
if self.imageCounter >= self.imageData.count {
self.imageCounter = 0
}
cell.image.image = UIImage(named: currImage)
return cell
}
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 26
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: 90, height: 90)
}
}
I hope I am missing some small bit of code to solve this problem, but after many hours of searching on Stackoverflow, and the internet, I still can not find a solution to this problem. If anyone has a solution or tip, it would be greatly appreciated!
Greetings
The issue is that you're using imageCounter as the index into your array of images, but incrementing it instead of using the indexPath.item. Remember that the UICollectionView will reuse UICollectionViewCell instances. Basically, it will only create cell instances for those that are on the screen. If a cell scrolls off the screen and a new one takes its place (e.g. if you have "A", "B", and "C" on the screen, and scroll down so you see "B", "C", "D", the UICollectionView will reuse the "A" cell for "D". This is a bit of an oversimplification, but more or less how it works). As such, the cellForItem call will give you the information about which cell is being displayed in the indexPath parameter. In this case, you can get rid of all of the self.imageCounter logic and instead do cell.image.image = UIImage(named: imageData[indexPath.row])
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellid", for: indexPath) as! MyImageCell
cell.backgroundColor = UIColor.white
cell.image.image = UIImage(named: imageData[indexPath.row])
return cell
}