I have a UITableView which loads images from a firebase database. Each cell in the table contains three pictures. The firestore query loads three documents at a time, while the table view paginates when the users scroll to the bottom. The issue I am having is that as I scroll the table view stutters every time it reaches a new cell. Each cell takes up a little more than the full screen. Here is an example of what I am trying to describe: https://imgur.com/a/xRB6gZg
Here is the code that is producing these issues:
func paginate(){
postQuery = postQuery.start(afterDocument: documents.last!)
self.loadPosts()
}
//queries Firestore and loads into postArray
func loadPosts() {
if let blockedArray = userDefaults.array(forKey: blockKey) as? [String]{
blockedUsers = blockedArray
}
postQuery.getDocuments{ [weak self](querySnapshot, error) in
self!.q.async{
if let err = error {
print(err)
}else{
var postsTemp = self?.postArray
for doc in querySnapshot!.documents{
self?.documents += [doc]
let post = self!.createPost(doc)
if(!self!.postArray.contains(post) && !self!.blockedUsers.contains(post.uid)){
postsTemp?.append(post)
}
DispatchQueue.main.async {
self!.postArray = postsTemp!
self!.tableView.reloadData()
self!.isNewDataLoading = false
}
}
self!.loadedFirst = true
}
}
}
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if postArray.count == 0{
return 1
}else{
return postArray.count
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
var post: PostStruct
var peopleUserIsFollowing: [String] = []
let cell = tableView.dequeueReusableCell(withIdentifier: K.cellIdentifier, for: indexPath) as! PostCell
cell.delegate = self
if postArray.count == 0 {
let instructions = cell.textLabel
instructions?.text = "Press the camera to start Piking!"
instructions?.textAlignment = .center
clearPosts(cell)
}else {
post = postArray[indexPath.row]
if let leftPostArray = userDefaults.array(forKey: fbLeftKey) as? [String]{
votedLeftPosts = leftPostArray
}
if let rightPostArray = userDefaults.array(forKey: fbRightKey) as? [String]{
votedRightPosts = rightPostArray
}
let firstReference = storageRef.child(post.firstImageUrl)
let secondReference = storageRef.child(post.secondImageUrl)
//For FriendsTableView query
let db = Firestore.firestore()
let followingReference = db.collection("following")
.document(currentUser!)
.collection("UserIsFollowing")
followingReference.getDocuments(){(querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
for document in querySnapshot!.documents {
peopleUserIsFollowing.append(document.documentID)
}
}
}
//Fill in labels and imageViews
cell.timer = createTimer(post, cell)
cell.firstImageView.sd_setImage(with: firstReference)
cell.secondImageView.sd_setImage(with: secondReference)
cell.leftTitle.text = post.firstTitle
cell.rightTitle.text = post.secondTitle
cell.postDescription.text = post.postDescription + "\(indexPath)"
if post.userPic == "" {
userPic =
"https://firebasestorage.googleapis.com/v0/b/pikit-7e40e4.appspot.com/o/Default%20Profile%20Pic.png?alt=media&token=2bc88382-2ad3-4eb8-8163-dcddf391c666"
} else{
userPic = post.userPic
}
let url = URL(string: userPic)
let data = try? Data(contentsOf: url!)
cell.profilePic.image = UIImage(data: data!)
let votesCollection = db.collection("votes").document(post.postID)
getCount(ref: votesCollection, cell: cell)
if(post.uid != currentUser){
cell.userName.text = post.poster
}else{
cell.userName.text = "Me"
cell.tapLeft.isEnabled = false
cell.tapRight.isEnabled = false
}
cell.textLabel?.text = ""
if(post.poster == Auth.auth().currentUser!.uid || post.endDate - Int(NSDate().timeIntervalSince1970) <= 0){
cell.tapRight.isEnabled = false
cell.tapLeft.isEnabled = false
cell.firstImageView.layer.borderWidth = 0
cell.secondImageView.layer.borderWidth = 0
}
else if(votedRightPosts.contains(post.firstImageUrl)){
cell.secondImageView.layer.borderColor = UIColor.green.cgColor
cell.secondImageView.layer.borderWidth = 4
cell.firstImageView.layer.borderWidth = 0
cell.tapRight.isEnabled = false
cell.tapLeft.isEnabled = true
}
else if (votedLeftPosts.contains(post.firstImageUrl)){
cell.firstImageView.layer.borderColor = UIColor.green.cgColor
cell.firstImageView.layer.borderWidth = 4
cell.secondImageView.layer.borderWidth = 0
cell.tapLeft.isEnabled = false
cell.tapRight.isEnabled = true
}
}
return cell
}
override func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let postCell = tableView.dequeueReusableCell(withIdentifier: K.cellIdentifier, for: indexPath) as! PostCell
clearPosts(postCell)
postCell.timer?.invalidate()
postCell.timer = nil
}
override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
//Bottom Refresh
if scrollView == tableView{
if ((scrollView.contentOffset.y + scrollView.frame.size.height) >= scrollView.contentSize.height)
{
if !isNewDataLoading{
isNewDataLoading = true
paginate()
}
}
}
}
I have tried adjusting what didEndDisplaying does, such as clearing cells/ not clearing cells, but that had no effect. I have also tried changing around where paginate is called but this seems to be the best way. I am not sure where I went wrong. I have also noticed in the Xcode debugger that the memory usage of the app steadily rises as the table view is scrolled up and down, but never seems to go down.
In general, you have two options to fix this problem. That's a lot of code to parse through, so I can't give you a code sample, but the answers are either:
Pre-fetching
When you scroll to item 2, kick off the fetch for items 4,5,6 (since you fetch 3 at a time) before you scroll down that far.
Also... you might consider fetching more than 3 at a time. Like... 50, or 100. Modern iOS devices have lots of memory. No real reason I can think of to limit it to so few.
Placeholders
Build your layout so it gives placeholder data and then kick off the fetch asynchronously to update the on-screen layout with the real data.
Either way is going to require you to restructure your code a bit. My intuition says that pre-fetching is going to be easier for you.
Related
I have an app that saves an array of 5 randomly generated colors via CloudKit. They are saved under Field Type of String (list). I'm able to save and retrieve the colors successfully.
In my app, I want to display each record with the array of colors on a different row. Currently, the correct number of rows show when data is retrieved, but only the first row has the array of colors (screenshots below). When I pull down on the table, it refreshes the rows and will show a different color array that was saved.
import UIKit
import CloudKit
class FavoritesController: UIViewController, UITableViewDataSource {
let paletteController = PaletteController()
let favoritesTableView = UITableView()
let reuseIdentifier = "favoritesCell"
let privateDatabase = CKContainer.default().privateCloudDatabase
var retrieveFavoritePalette: [CKRecord] = []
override func viewDidLoad() {
super.viewDidLoad()
setupTableView()
queryDatabase()
}
func setupTableView() {
favoritesTableView.dataSource = self
favoritesTableView.delegate = self
favoritesTableView.register(UITableViewCell.self, forCellReuseIdentifier: reuseIdentifier)
let heightOfCells: CGFloat = 100
favoritesTableView.rowHeight = heightOfCells
view.addSubview(favoritesTableView)
favoritesTableView.anchor(top: view.topAnchor, left: view.leftAnchor, bottom: view.bottomAnchor, right: view.rightAnchor)
}
func queryDatabase() {
let query = CKQuery(recordType: "Favorite", predicate: NSPredicate(value: true))
privateDatabase.perform(query, inZoneWith: nil) { (records, error) in
if error == nil {
print("Record retrieved")
for record in records! {
self.retrieveFavoritePalette.append(record)
}
} else {
print("Record not retrieved: \(String(describing: error))")
}
let sortedRecords = records?.sorted(by: { $0.creationDate! > $1.creationDate! })
self.retrieveFavoritePalette = sortedRecords ?? []
DispatchQueue.main.async {
self.favoritesTableView.reloadData()
}
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return retrieveFavoritePalette.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath)
let paletteRecord: CKRecord = retrieveFavoritePalette[indexPath.row]
var individualColorView: [UIView] = []
// print(paletteRecord.value(forKey: "FavoritePalette"))
let line = paletteRecord.value(forKey: "FavoritePalette") as? [String] ?? []
//Creates the individual boxes where the color goes
for i in 0..<5 {
let xAxis = i * 20
let individualView = UIView(frame: CGRect(x: xAxis, y: 0, width: 20, height: 80))
individualColorView.append(individualView)
}
for j in 0..<line.count {
let allColorsView = individualColorView[j]
print(individualColorView[j])
allColorsView.backgroundColor = UIColor(hexString: line[j])
tableView.addSubview(allColorsView)
}
cell.selectionStyle = .none
return cell
}
}
I tried putting let paletteRecord... to tableView.addSubview(allColorsView) into a TableViewCell class, but I got stuck when I couldn't figure out how to have the code compile without indexPath.row in let paletteRecord: CKRecord = retrieveFavoritePalette[indexPath.row].
Output of print(paletteRecord.value(forKey: "FavoritePalette")) is Optional(<__NSArrayM 0x6000039f98f0>( BBBB88, CCC68D, EEDD99, EEC290, EEAA88 ))
This is what it currently looks like. I need each row to display the string array that was saved to CloudKit.
Data that's saved to CloudKit
Any help is appreciated!
I think your issue is on this line:
tableView.addSubview(allColorsView)
You are adding your color boxes to the tableView, but you probably meant to add them to the cell itself like this:
cell.addSubview(allColorsView)
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to delete item 30 from section 0 which only contains 27 items before the update'
I am facing a problem when collection view reloads my app is getting crash. My code logic is, I am displaying some cells and for every 10 cells am adding one extra cell for advertisement and i think here am getting problem because the indexpath is missing when collection view reloads. I have googled and got some information regarding my crash issue . Some of it are,
The documentation for performBatchUpdates:completion states:
Deletes are processed before inserts in batch operations. This means the indexes for the deletions are processed relative to the indexes of the collection view’s state before the batch operation, and the indexes for the insertions are processed relative to the indexes of the state after all the deletions in the batch operation.
I cannot able to implement it in my code. Can anyone please help me in this?
Thanks.
override func viewDidLoad(){
super.viewDidLoad()
DispatchQueue.main.async {
self.viewModel.tilbudsappenModel.addProduct(userId: self.userInfo.userID, shopId: self.shopId)
self.m_CollectionVw.reloadData()
}
}
override func viewWillAppear(_ animated: Bool){
SwiftEventBus.onMainThread(self, name: "ReloadTblVw") { _ in
self.m_CollectionVw.reloadData()
}
if self.isFollowed == true {
viewModel.tilbudsappenModel.removeProduct()
DispatchQueue.main.async {
self.viewModel.tilbudsappenModel.addProduct(userId: self.userInfo.userID, shopId: self.shopId)
self.m_CollectionVw.reloadData()
}
}
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// Here am getting the total items count
numberOfOffers = viewModel.tilbudsappenModel.adNativeModel.totalItems()
let totalNoOfOffers = numberOfOffers + (numberOfOffers / numberOfAds)
return totalNoOfOffers
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if indexPath.row != 0 && indexPath.row % numberOfAds == 0 && !AppUserDefaults.SharedInstance.isSubscripted {
let nativeAd: GADUnifiedNativeAd? = nil
/// Set the native ad's rootViewController to the current view controller.
nativeAd?.rootViewController = self
let nativeAdCell = collectionView.dequeueReusableCell(withReuseIdentifier: "UnifiedNativeAdCell", for: indexPath)
// Get the ad view from the Cell. The view hierarchy for this cell is defined in
// UnifiedNativeAdCell.xib.
let adView : GADUnifiedNativeAdView = nativeAdCell.contentView.subviews
.first as! GADUnifiedNativeAdView
if self.nativeAds.count > indexPath.row / numberOfAds {
let nativeAd = self.nativeAds[indexPath.row / numberOfAds]
adView.nativeAd = nativeAd
nativeAd.delegate = self
(adView.headlineView as? UILabel)?.text = nativeAd.headline
adView.mediaView?.mediaContent = nativeAd.mediaContent
// These assets are not guaranteed to be present. Check that they are before
// showing or hiding them.
(adView.bodyView as? UILabel)?.text = nativeAd.body
adView.bodyView?.isHidden = nativeAd.body == nil
(adView.callToActionView as? UIButton)?.setTitle(nativeAd.callToAction, for: .normal)
adView.callToActionView?.isHidden = nativeAd.callToAction == nil
(adView.iconView as? UIImageView)?.image = nativeAd.icon?.image
adView.iconView?.isHidden = nativeAd.icon == nil
(adView.starRatingView as? UIImageView)?.image = self.imageOfStars(from:nativeAd.starRating)
adView.starRatingView?.isHidden = nativeAd.starRating == nil
(adView.storeView as? UILabel)?.text = nativeAd.store
adView.storeView?.isHidden = nativeAd.store == nil
(adView.priceView as? UILabel)?.text = nativeAd.price
adView.priceView?.isHidden = nativeAd.price == nil
(adView.advertiserView as? UILabel)?.text = nativeAd.advertiser
adView.advertiserView?.isHidden = nativeAd.advertiser == nil
// In order for the SDK to process touch events properly, user interaction should be disabled.
adView.callToActionView?.isUserInteractionEnabled = false
}
return nativeAdCell
} else {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! ShopOfferCell
cellIndex = indexPath.item - indexPath.item / numberOfAds
if let offerContent = viewModel.tilbudsappenModel.getProductItem(index: cellIndex) {
cell.setContent(content: offerContent)
if offerContent.imageWidth < Int(cell.contentView.frame.size.width) {
cell.m_ImgVw.contentMode = .center
}else {
cell.m_ImgVw.contentMode = .scaleAspectFit
}
}
return cell
}
}
I have question about the tableView.
Here is my tableView code
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tierCount
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "InterestRateTableViewCell"
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? InterestRateTableViewCell else {
fatalError("The dequed cell is not an instance of InterestRateTableViewCell.")
}
cell.interestRateTextField.delegate = self
cell.rowLabel.text = "\(indexPath.row + 1)."
if let interestText = cell.interestRateTextField.text {
if let interest = Double(interestText){
interestRateArray[indexPath.row] = interest
} else {
interestRateArray[indexPath.row] = nil
}
} else {
interestRateArray[indexPath.row] = nil
}
return cell
}
As you can see, I have the cellForRowAt method to get the value from the textfields in the cell, and assign to my arrays. (I actually have 2 textfields per cell.)
Basically, I let the users input and edit the textfield until they are happy then click this calculate button, which will call the calculation method. In the calculation method I call the "tableView.reloadData()" first to gather data from the textfields before proceed with the actual calculation.
The problem was when I ran the app. I typed values in all the textfields then clicked "calculate", but it showed error like the textfields were still empty. I clicked again, and it worked. It's like I had to reload twice to get things going.
Can anyone help me out?
By the way, please excuse my English. I'm not from the country that speak English.
edited: It may be useful to post the calculate button code here as someone suggested. So, here is the code of calculate button
#IBAction func calculateRepayment(_ sender: UIButton) {
//Reload data to get the lastest interest rate and duration values
DispatchQueue.main.async {
self.interestRateTableView.reloadData()
}
//Get the loan value from the text field
if let loanText = loanTextField.text {
if let loanValue = Double(loanText) {
loan = loanValue
} else {
print("Can not convert loan value to type Double.")
return
}
} else {
print("Loan value is nil")
return
}
tiers = []
var index = 0
var tier: Tier
for _ in 0..<tierCount {
if let interestRateValue = interestRateArray[index] {
if let durationValue = durationArrayInMonth[index] {
tier = Tier(interestRateInYear: interestRateValue, tierInMonth: durationValue)
tiers.append(tier)
index += 1
} else {
print("Duration array contain nil")
return
}
} else {
print("Interest rate array contain nil")
return
}
}
let calculator = Calculator()
repayment = calculator.calculateRepayment(tiers: tiers, loan: loan!)
if let repaymentValue = repayment {
repaymentLabel.text = "\(repaymentValue)"
totalRepaymentLabel.text = "\(repaymentValue * Double(termInYear!) * 12)"
} else {
repaymentLabel.text = "Error Calculating"
totalRepaymentLabel.text = ""
}
}
cellForRowAt is used for initially creating and configuring each cell, so the textfields are empty when this method is called.
UITableView.reloadData() documentation:
// Reloads everything from scratch. Redisplays visible rows. Note that this will cause any existing drop placeholder rows to be removed.
open func reloadData()
As it says in Apple's comment above, UITableView.reloadData() will reload everything from scratch. That includes your text fields.
There are a number of ways to fix your issue, but it's hard to say the best way without more context. Here's an example that would fit the current context of your code fairly closely:
class MyCustomTableViewCell: UITableViewCell {
#IBOutlet weak var interestRateTextField: UITextField!
var interestRateChangedHandler: (() -> ()) = nil
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
interestRateTextField.addTarget(self, action: #selector(interestRateChanged), for: UIControlEvents.editingChanged)
}
#objc
func interestRateChanged() {
interestRateChangedHandler?()
}
}
and cellForRowAtIndex:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellIdentifier = "InterestRateTableViewCell"
guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as? InterestRateTableViewCell else {
fatalError("The dequed cell is not an instance of InterestRateTableViewCell.")
}
cell.rowLabel.text = "\(indexPath.row + 1)."
cell.interestRateChangedHandler = { [weak self] in
if let interestText = cell.interestRateTextField.text {
if let interest = Double(interestText){
self?.interestRateArray[indexPath.row] = interest
} else {
self?.interestRateArray[indexPath.row] = nil
}
} else {
self?.interestRateArray[indexPath.row] = nil
}
}
return cell
}
So I have 3 cells: SenderCell, ReceiverCell , and a default blank Cell
I want the mainTableView to show all messages from both sender and receiver, which are contained in messageFromCurrentUserArray and messageFromReceiverArray respectively. Unfortunately, whenever I run the code below, it only shows either cell. I would like to show both cells if their arrays aren't empty. Hoping someone could help me. Thanks in advance!
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return allMessages.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row < messageFromCurrentUserArray.count {
let cell = mainTableView.dequeueReusableCell(withIdentifier: "SenderCell", for: indexPath) as! SenderTableViewCell
cell.senderMessage.text = messageFromCurrentUserArray[indexPath.row]
mainTableView.beginUpdates()
cell.senderMessage.sizeToFit()
let imageFile = PFUser.current()?["profilePhoto"] as! PFFile
imageFile.getDataInBackground(block: { (data, error) in
if let imageData = data {
self.currentUserImage = UIImage(data: imageData)!
}
})
cell.senderProfilePhoto.image = currentUserImage
cell.senderProfilePhoto.layer.masksToBounds = false
cell.senderProfilePhoto.layer.cornerRadius = cell.senderProfilePhoto.frame.height / 2
cell.senderProfilePhoto.clipsToBounds = true
mainTableView.endUpdates()
return cell
} else if indexPath.row < messageFromReceiverArray.count {
let cell = mainTableView.dequeueReusableCell(withIdentifier: "ReceiverCell", for: indexPath) as! ReceiverTableViewCell
cell.receiverMessage.text = messageFromReceiverArray[indexPath.row]
mainTableView.beginUpdates()
cell.receiverMessage.sizeToFit()
cell.receiverProfilePhoto.image = receiverImage
cell.receiverProfilePhoto.layer.masksToBounds = false
cell.receiverProfilePhoto.layer.cornerRadius = cell.receiverProfilePhoto.frame.height / 2
cell.receiverProfilePhoto.clipsToBounds = true
mainTableView.endUpdates()
return cell
} else {
let cell = mainTableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! CellTableViewCell
return cell
}
}
The problem is the comparsion in the outer if statements:
if indexPath.row < messageFromCurrentUserArray.count {
//...
} else if indexPath.row < messageFromReceiverArray.count {
// ...
} else {
// ...
}
Since all the counts in the arrays start with index 0 and stop at their count each, but your indexPath.row will go up to the the sum of both (e.g. messageFromCurrentUserArray.count + messageFromReceiverArray.count)
You'll have to change the second if clause and the index access to something like
} else if indexPath.row < (messageFromCurrentUserArray.count + messageFromReceiverArray.count) {
// ...
int receiverIndex = indexPath.row - messageFromCurrentUserArray.count
cell.receiverMessage.text = messageFromReceiverArray[receiverIndex]
// or: cell.receiverMessage.text = allMessages[indexPath.row]
// ...
} else {
// ...
}
I have been following a tutorial online for a yikyak clone using swift and parse. i am storing the objectIDs of the upvoted/downvoted items using coreData. When the tableview cell is loaded, it checks if the objectID on parse is in coreData and responds accordingly by adding a background image to the specific button and disabling both the up and down vote buttons. However, I am facing an issue where scrolling up and down a few times causes random cells to have the background and have their buttons disabled.
Here is a link to the code (cellForRowAtIndexPath:):
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as PullTableViewCell
let restaurant = self.restaurantData[indexPath.row]
cell.scoreLabel.text = "\(score)"
cell.plusButton.tag = indexPath.row;
cell.plusButton.addTarget(self, action: "plusOne:", forControlEvents: .TouchUpInside)
cell.minusButton.tag = indexPath.row
cell.minusButton.addTarget(self, action: "minusOne:", forControlEvents: .TouchUpInside)
if (cell.plusButton.enabled) {
// upvotes
let request = NSFetchRequest(entityName: "Upvotes")
let moc:NSManagedObjectContext = self.managedObjectContext!
var error: NSErrorPointer = nil
self.upvoteData = moc.executeFetchRequest(request, error: error) as [Upvotes]
for (var i = 0; i < self.upvoteData.count; i++) {
if (self.upvoteData[i].objectid == self.objectIDs[indexPath.row]) {
NSLog("the cell is in view is \(indexPath.row)")
cell.plusButton.backgroundColor = UIColor.blueColor()
cell.minusButton.enabled = false
cell.plusButton.enabled = false
}
}
// downvotes
let request2 = NSFetchRequest(entityName: "Downvotes")
let moc2:NSManagedObjectContext = self.managedObjectContext!
var error2: NSErrorPointer = nil
self.downvoteData = moc2.executeFetchRequest(request2, error: error2) as [Downvotes]
for (var i = 0; i < self.downvoteData.count; i++) {
if (self.downvoteData[i].objectid == self.objectIDs[indexPath.row]) {
cell.minusButton.backgroundColor = UIColor.blueColor()
cell.minusButton.enabled = false
cell.plusButton.enabled = false
}
}
}
return cell
}
Does it have to do with asynchronous processing?
The tableview actually re-creates cells on the fly as you scroll up and down. So when a cell is scrolled out of view it can get destroyed and re-created.
You need to store cell properties inside of a map and then re-initialize the cell each time.
Here is an example from my own code:
public func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
{
var cell = collectionView.dequeueReusableCellWithReuseIdentifier("selectPhotoCell", forIndexPath: indexPath) as B_SelectPhotoControllerViewCell
cell.indexPath = indexPath
let asset = currentAssetAtIndex(indexPath.item)
PHImageManager.defaultManager().requestImageForAsset(asset, targetSize:_cellSize, contentMode: .AspectFit, options: nil)
{
result, info in
if(cell.indexPath == indexPath)
{
cell.imageView.image = result
cell.imageView.frame = CGRectMake(0, 0,self._cellSize.width,self._cellSize.height)
for editImage in self._editImageChicklets
{
if(editImage.selectedPhotoIndexPath != nil && editImage.selectedPhotoIndexPath == indexPath)
{
cell.number = editImage.selectedPhotoDisplayNumber!
}
}
}
}
return cell
}
This is an example of a UICollectionView that uses photo images from the users photo roll. I keep track of the indexPath, and if its a match, then I assign the images to the property.
In your case you need to do the same. Keep a map of the cell properties and the index path:
Dictionary<NSIndexPath,CustomCellPropertiesObject>
Then go through and re-initialize it appropriately.