How to get the indexPath of the current cell - ios

I have currently trying to create a way to delete an item in a collection view at the indexPath of 1 back. So far i have used some help to create a function with scrollview did scroll to create a way to count which image the user is on by the current image method. I now need a way to count which cell the user is on. Here is my code>
var currentImage = 0
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let x = floor(myCollectionView.contentOffset.x / view.frame.width)
if Int(x) != currentImage {
currentImage = Int(x)
//print(currentImage)
}
if currentImage > 0 {
for collectionCell in myCollectionView.visibleCells as [UICollectionViewCell] {
let indexPath = myCollectionView.indexPath(for: collectionCell as UICollectionViewCell)!
let indexPathOfLastItem = (indexPath?.item)! - 1
let indexPathOfItemToDelete = IndexPath(item: (indexPathOfLastItem), section: 0)
imageArray.remove(at: 0)
myCollectionView.deleteItems(at: [indexPathOfItemToDelete])
currentImage = 1

Based more on the comments than your actual question, what you seem to want is to get the first visible cell's index path so you can use that path to delete the cell.
let visibleCells = myCollectionView.visibleCells
if let firstCell = visibleCells.first() {
if let indexPath = myCollectionView.indexPath(for: collectionCell as UICollectionViewCell) {
// use indexPath to delete the cell
}
}
None of this should be used or done in the scrollViewDidScroll delegate method.

[Updated for Swift 5.2] Here's a slightly more succinct way than #rmaddy's answer (but that one works just fine):
guard let indexPath = collectionView.indexPathsForVisibleItems.first else {
return
}
// Do whatever with the index path here.

Related

Deleting Items out of collection view

I have currently created an array of images [1 - 8]. I have also created a collectionview that pulls images out of the imageArray and puts images in the imageview which is in the cell. Please note that the cell has one imageView in it and it takes up the whole screen, horizontal scrolling and paging are both enabled.
Right now with the current code I have, is very odd what is currently happening so I will tell you what is currently going on. So what currently is happening (I'm going by image 1 which index 0) if the image is on 2 (index 1) and then you swipe next to 3 (index 2), it skips image 3 (index 2) and 4 (index 3) and sits on image 5 (index 4), so when I mean skips, I mean it slides past the image you just swiped to and one more.
(Oddly it deletes image 1 and 2) once on 5. I believe this is due to the it is updating the index or setting it to 2 over again because it just deleted 0. I know this might be hard to get but just think of scrollview with paging and when you swipe to the third image, it skips the one your on and one more and slides you to image 5 where it stays place.
So far thanks to some of you, I have came up with this code below, but I am hoping someone will be able to solve this awful mess.
var currentImage = 0
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let x = floor(myCollectionView.contentOffset.x / view.frame.width)
if Int(x) != currentImage {
currentImage = Int(x)
}
if currentImage > 1 {
for collectionCell in myCollectionView.visibleCells as [UICollectionViewCell] {
let visibleCells = myCollectionView.visibleCells
if visibleCells.first != nil {
if let indexPath = myCollectionView.indexPath(for: collectionCell as UICollectionViewCell) {
let indexPathOfLastItem = (indexPath.item) - 1
let indexPathOfItemToDelete = IndexPath(item: (indexPathOfLastItem), section: 0)
imageArray.remove(at: (indexPath.item) - 1)
myCollectionView.deleteItems(at: [indexPathOfItemToDelete])
}
}
}
}
}
These codes will delete a specific item using the item index. This is working great!
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
var visibleRect = CGRect()
visibleRect.origin = myCollectionView.contentOffset
visibleRect.size = myCollectionView.bounds.size
let visiblePoint = CGPoint(x: visibleRect.midX, y: visibleRect.midY)
let visibleIndexPath: IndexPath = myCollectionView.indexPathForItem(at: visiblePoint)!
print(visibleIndexPath)
fruitArray.remove(at: visibleIndexPath.row )
myCollectionView.deleteItems(at: [visibleIndexPath])
myCollectionView.scrollToItem(at: IndexPath.init(row: visibleIndexPath.row-1, section: 0), at: UICollectionViewScrollPosition.centeredHorizontally, animated: false)
}
Use scrollViewDidEndDragging to detect when user stops scrolling and delete the cell,..
Objective C
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
// if decelerating, let scrollViewDidEndDecelerating: handle it
if (decelerate == NO) {
[self deleteCell];
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
[self deleteCell];
}
- (void)deleteCell {
NSIndexPath *pathForCenterCell = [self.tableView indexPathForRowAtPoint:CGPointMake(CGRectGetMidX(self.tableView.bounds), CGRectGetMidY(self.tableView.bounds))]; // if you want middle cell
NSIndexPath *firstVisibleIndexPath = [[self.tableView indexPathsForVisibleRows] objectAtIndex:0]; // if you want first visible cell
NSIndexPath *lastVisibleIndexPath = [[self.tableView indexPathsForVisibleRows] objectAtIndex:[self.tableView indexPathsForVisibleRows].count]; // last cell in visible cells
myCollectionView.beginUpdates() //if you are performing more than one operation use this
yourImage.removeObjectAtIndex(myNSIndexPath.row)
myCollectionView.deleteItems(at: [lastVisibleIndexPath])
myCollectionView.endUpdates()
}
Swift 3.0
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
// if decelerating, let scrollViewDidEndDecelerating: handle it
if decelerate == false {
deleteCell()
}
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
deleteCell()
}
func deleteCell() {
var pathForCenterCell: IndexPath? = tableView.indexPathForRow(at: CGPoint(x: CGFloat(tableView.bounds.midX), y: CGFloat(tableView.bounds.midY)))
// if you want middle cell
var firstVisibleIndexPath: IndexPath? = (tableView.indexPathsForVisibleRows?[0] as? IndexPath)
// if you want first visible cell
var lastVisibleIndexPath: IndexPath? = (tableView.indexPathsForVisibleRows?[tableView.indexPathsForVisibleRows?.count] as? IndexPath)
// last cell in visible cells
myCollectionView.beginUpdates()()
//if you are performing more than one operation use this
yourImage.removeObjectAtIndex(myNSIndexPath.row)
myCollectionView.deleteItems
}
Happy coding

Multiple Collection Views with same XIB Cell and button

I have a View Controller with multiple Collection Views.
Each collection view is using the same custom cell with xib. In this xib i have a button.
The collectionViews names are
1) manPerfumeCV
2) womanPerfumeCV
3) kidsPerfumeCV
Inside cellForItemAt i have cell.productCart.tag = indexPath.row
let cell:iPhoneCollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: "iPhoneCell", for: indexPath) as! iPhoneCollectionViewCell
if collectionView == self.womanPerfumeCV {
let prods = womanPerfumeData[indexPath.row]
cell.configureCell(products: prods)
cell.productCart.tag = indexPath.row
} else if collectionView == self.manPerfumeCV {
let prods = manPerfumeData[indexPath.row]
cell.configureCell(products: prods)
cell.productCart.tag = indexPath.row
} else if collectionView == self.kidsPerfumeCV {
let prods = kidsPerfumeData[indexPath.row]
cell.configureCell(products: prods)
cell.productCart.tag = indexPath.row
}
and in the same view controller i have this action for the button from the xib file
#IBAction func iPhoneAddToCart(_ sender: AnyObject) {
let butt = sender as! UIButton
let indexP = IndexPath(row: butt.tag, section: 0)
let cell = manPerfumeCV.cellForItem(at: indexP) as! iPhoneCollectionViewCell
print(manPerfumeData[indexP.row].price)
}
Each collection view has its own [Products] array.
1) manPerfumeData
2) womanPerfumeData
3) kidPerfumeData.
In my code if i tap at the button which is at the 1st collection view with manPerfumeData it prints the price very well.
Although if i push the button on the 2nd or 3rd collection view the app crashes.
Is there any way to know from wich collection view he pushed the button so i can load the spesific [Products] array ??
Thanks!
You can use tag property of UIButton.
Let consider your code
let cell:iPhoneCollectionViewCell = collectionView.dequeueReusableCell(withReuseIdentifier: "iPhoneCell", for: indexPath) as! iPhoneCollectionViewCell
if collectionView == self.womanPerfumeCV {
//Your Code
cell.productCart.tag = indexPath.row + 1000
} else if collectionView == self.manPerfumeCV {
//Your Code
cell.productCart.tag = indexPath.row + 2000
} else if collectionView == self.kidsPerfumeCV {
//Your Code
cell.productCart.tag = indexPath.row + 3000
}
You noticed that I changed the tag property of UIButton. Now when tap on button, check tag property of UIButton. Like this
if (sender.tag >= 1000 && sender.tag<2000)
{
//manPerfumeCV
}
else if (sender.tag >= 2000 && sender.tag<3000)
{
//womanPerfumeCV
}
else
{
//kidsPerfumeCV
}
For fetching value from array, subtract the beginning value from tag property and you will get value, like this
For manPerfumeCV
indexValue = sender.tag - 1000
For womanPerfumeCV
indexValue = sender.tag - 2000
For kidsPerfumeCV
indexValue = sender.tag - 3000
This value can directly use for getting data from array.
inorder to achieve it you can add tag property to the collectionViews in viewDidLoad
override func viewDidLoad() {
// your code
self.womanPerfumeCV.tag = 0;
self.manPerfumeCV.tag = 1;
self.kidsPerfumeCV = 2;
}
using this you can identify the source
// Now its time to listen to the buttonEvent
I would suggest to keep the event listener in the same controller or make a protocol callback only with index and tag. To keep it simple lets make the changes in same viewController File
if collectionView == self.womanPerfumeCV {
let prods = womanPerfumeData[indexPath.row]
cell.configureCell(products: prods)
cell.productCart.tag = indexPath.row
} else if collectionView == self.manPerfumeCV {
let prods = manPerfumeData[indexPath.row]
cell.configureCell(products: prods)
cell.productCart.tag = indexPath.row
} else if collectionView == self.kidsPerfumeCV {
let prods = kidsPerfumeData[indexPath.row]
cell.configureCell(products: prods)
cell.productCart.tag = indexPath.row
}
cell.yourButton.tag = (collectionView.tag * 1000 )+(indexPath.row);
cell.yourButton.addTarget(self, action: #selector(ButtonClicked(_:)), forControlEvents: .TouchUpInside)
now add selector to event
func ButtonClicked(sender: UIButton) {
let index : Int = sender.tag % 1000;
switch sender.tag {
case let x where x < 1000:
loadDataFromFirstArrayWithIndex(index);break;
case let x where x <2000:
loadDataFromSecondArrayWithIndex(index);break;
default:
loadDataFromThirdArrayWithIndex(index);
}
}

Swiping left to create a new collectionViewCell

I'm trying to implement a smoother way for a use to add new collectionViewCells for myCollectionView (which only shows one cell at a time). I want it to be something like when the user swipes left and if the user is on the last cell myCollectionView inserts a new cell when the user is swiping so that the user swipes left "into" the cell. And also I only allow the user to scroll one cell at a time.
EDIT:
So I think it is a bit hard to describe it in words so here is a gif to show what I mean
So in the past few weeks I have been trying to implement this in a number of different ways, and the one that I have found the most success with is by using the scrollViewWillEndDragging delegate method and I have implemented it like this:
func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
// Getting the size of the cells
let flowLayout = myCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
let cellWidth = flowLayout.itemSize.width
let cellPadding = 10.0 as! CGFloat
// Calculating which page "card" we should be on
let currentOffset = scrollView.contentOffset.x - cellWidth/2
print("currentOffset is \(currentOffset)")
let cardWidth = cellWidth + cellPadding
var page = Int(round((currentOffset)/(cardWidth) + 1))
print("current page number is: \(page)")
if (velocity.x < 0) {
page -= 1
}
if (velocity.x > 0) {
page += 1
}
print("Updated page number is: \(page)")
print("Previous page number is: \(self.previousPage)")
// Only allowing the user to scroll for one page!
if(page > self.previousPage) {
page = self.previousPage + 1
self.previousPage = page
}
else if (page == self.previousPage) {
page = self.previousPage
}
else {
page = self.previousPage - 1
self.previousPage = page
}
print("new page number is: " + String(page))
print("addedCards.count + 1 is: " + String(addedCards.count + 1))
if (page == addedCards.count) {
print("reloading data")
// Update data source
addedCards.append("card")
// Method 1
cardCollectionView.reloadData()
// Method 2
// let newIndexPath = NSIndexPath(forItem: addedCards.count - 1, inSection: 0)
// cardCollectionView.insertItemsAtIndexPaths([newIndexPath])
}
// print("Centering on new cell")
// Center the cardCollectionView on the new page
let newOffset = CGFloat(page * Int((cellWidth + cellPadding)))
print("newOffset is: \(newOffset)")
targetContentOffset.memory.x = newOffset
}
Although I think I have nearly got the desired result there are still some concerns and also bugs that I have found.
My main concern is the fact that instead of inserting a single cell at the end of myCollectionView I'm reloading the whole table. The reason that I do this is because if I didn't then myCollectionView.contentOffset wouldn't be changed and as a result when a new cell is created, myCollectionView isn't centered on the newly created cell.
1. If the user scrolls very slowly and then stops, the new cell gets created but then myCollectionView gets stuck in between two cells it doesn't center in on the newly created cell.
2. When myCollectionView is in between the second last cell and the last cell as a result of 1., the next time the user swipes right, instead of creating one single cell, two cells are created.
I've also used different ways to implement this behaviour such as using scrollViewDidScroll, and various other but to no avail. Can anyone point me in the right direction as I am kind of lost.
Here is a link to download my project if you want to see the interaction:
My Example Project
These the old methods if you're interested:
To do this I have tried 2 ways,
The first being:
func scrollViewWillEndDragging(scrollView: UIScrollView, withVelocity velocity: CGPoint,
targetContentOffset: UnsafeMutablePointer<CGPoint>) {
// page is the current cell that the user is on
// addedCards is the array that the data source works with
if (page == addedCards.count + 1) {
let placeholderFlashCardProxy = FlashCardProxy(phrase: nil, pronunciation: nil, definition: nil)
addedCards.append(placeholderFlashCardProxy)
let newIndexPath = NSIndexPath(forItem: addedCards.count, inSection: 0)
cardCollectionView.insertItemsAtIndexPaths([newIndexPath])
cardCollectionView.reloadData()
}
}
The problem with using this method is that:
Sometimes I will get a crash as a result of: NSInternalInconsistencyException', reason: 'Invalid update: invalid number of items in section 0.
When a new collectionCell is added it will sometimes display the input that the user has written from the previous cell (this might have sometimes to do with the dequeuing and re-use of cell, although again, I'm not sure and I would be grateful if someone answered it)
The inserting isn't smooth, I want the user to be able to swipe left at the last cell and "into" a new cell. As in if I am currently on the last cell, swiping left would put me automatically at the new cell, because right now when I swipe left a new cell is created by it doesn't center on the newly created cell
The second method that I am using is:
let swipeLeftGestureRecognizer = UISwipeGestureRecognizer(target: self, action: "swipedLeftOnCell:")
swipeLeftGestureRecognizer.direction = .Left
myCollectionView.addGestureRecognizer(swipeLeftGestureRecognizer)
swipeLeftGestureRecognizer.delegate = self
Although the swipe gesture very rarely responds, but if I use a tap gesture, myCollectionView always responds which is very weird (I know again this is a question on its own)
My question is which is the better way to implement what I have described above? And if none are good what should I work with to create the desired results, I've been trying to do this for two days now and I was wondering if someone could point me in the right direction. Thanks!
I hope this helps out in some way :)
UPDATE
I updated the code to fix the issue scrolling in either direction.
The updated gist can be found here
New Updated Gist
Old Gist
First I'm gonna define some model to for a Card
class Card {
var someCardData : String?
}
Next, create a collection view cell, with a card view inside that we will apply the transform to
class CollectionViewCell : UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(cardView)
self.addSubview(cardlabel)
}
override func prepareForReuse() {
super.prepareForReuse()
cardView.alpha = 1.0
cardView.layer.transform = CATransform3DIdentity
}
override func layoutSubviews() {
super.layoutSubviews()
cardView.frame = CGRectMake(contentPadding,
contentPadding,
contentView.bounds.width - (contentPadding * 2.0),
contentView.bounds.height - (contentPadding * 2.0))
cardlabel.frame = cardView.frame
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
lazy var cardView : UIView = {
[unowned self] in
var view = UIView(frame: CGRectZero)
view.backgroundColor = UIColor.whiteColor()
return view
}()
lazy var cardlabel : UILabel = {
[unowned self] in
var label = UILabel(frame: CGRectZero)
label.backgroundColor = UIColor.whiteColor()
label.textAlignment = .Center
return label
}()
}
Next setup the view controller with a collection view. As you will see there is a CustomCollectionView class, which I will define near the end.
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource {
var cards = [Card(), Card()]
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(collectionView)
collectionView.frame = CGRectMake(0, 0, self.view.bounds.width, tableViewHeight)
collectionView.contentInset = UIEdgeInsetsMake(0, 0, 0, contentPadding)
}
lazy var collectionView : CollectionView = {
[unowned self] in
// MARK: Custom Flow Layout defined below
var layout = CustomCollectionViewFlowLayout()
layout.contentDelegate = self
var collectionView = CollectionView(frame: CGRectZero, collectionViewLayout : layout)
collectionView.clipsToBounds = true
collectionView.showsVerticalScrollIndicator = false
collectionView.registerClass(CollectionViewCell.self, forCellWithReuseIdentifier: "CollectionViewCell")
collectionView.delegate = self
collectionView.dataSource = self
return collectionView
}()
// MARK: UICollectionViewDelegate, UICollectionViewDataSource
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return cards.count
}
func collectionView(collectionView : UICollectionView, layout collectionViewLayout:UICollectionViewLayout, sizeForItemAtIndexPath indexPath:NSIndexPath) -> CGSize {
return CGSizeMake(collectionView.bounds.width, tableViewHeight)
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cellIdentifier = "CollectionViewCell"
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(cellIdentifier, forIndexPath: indexPath) as! CollectionViewCell
cell.contentView.backgroundColor = UIColor.blueColor()
// UPDATE If the cell is not the initial index, and is equal the to animating index
// Prepare it's initial state
if flowLayout.animatingIndex == indexPath.row && indexPath.row != 0{
cell.cardView.alpha = 0.0
cell.cardView.layer.transform = CATransform3DScale(CATransform3DIdentity, 0.0, 0.0, 0.0)
}
return cell
}
}
UPDATED - Now For the really tricky part. I'm gonna define the CustomCollectionViewFlowLayout. The protocol callback returns the next insert index calculated by the flow layout
protocol CollectionViewFlowLayoutDelegate : class {
func flowLayout(flowLayout : CustomCollectionViewFlowLayout, insertIndex index : NSIndexPath)
}
/**
* Custom FlowLayout
* Tracks the currently visible index and updates the proposed content offset
*/
class CustomCollectionViewFlowLayout: UICollectionViewFlowLayout {
weak var contentDelegate: CollectionViewFlowLayoutDelegate?
// Tracks the card to be animated
// TODO: - Adjusted if cards are deleted by one if cards are deleted
private var animatingIndex : Int = 0
// Tracks thje currently visible index
private var visibleIndex : Int = 0 {
didSet {
if visibleIndex > oldValue {
if visibleIndex > animatingIndex {
// Only increment the animating index forward
animatingIndex = visibleIndex
}
if visibleIndex + 1 > self.collectionView!.numberOfItemsInSection(0) - 1 {
let currentEntryIndex = NSIndexPath(forRow: visibleIndex + 1, inSection: 0)
contentDelegate?.flowLayout(self, insertIndex: currentEntryIndex)
}
} else if visibleIndex < oldValue && animatingIndex == oldValue {
// if we start panning to the left, and the animating index is the old value
// let set the animating index to the last card.
animatingIndex = oldValue + 1
}
}
}
override init() {
super.init()
self.minimumInteritemSpacing = 0.0
self.minimumLineSpacing = 0.0
self.scrollDirection = .Horizontal
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// The width offset threshold percentage from 0 - 1
let thresholdOffsetPrecentage : CGFloat = 0.5
// This is the flick velocity threshold
let velocityThreshold : CGFloat = 0.4
override func targetContentOffsetForProposedContentOffset(proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
let leftThreshold = CGFloat(collectionView!.bounds.size.width) * ((CGFloat(visibleIndex) - 0.5))
let rightThreshold = CGFloat(collectionView!.bounds.size.width) * ((CGFloat(visibleIndex) + 0.5))
let currentHorizontalOffset = collectionView!.contentOffset.x
// If you either traverse far enought in either direction,
// or flicked the scrollview over the horizontal velocity in either direction,
// adjust the visible index accordingly
if currentHorizontalOffset < leftThreshold || velocity.x < -velocityThreshold {
visibleIndex = max(0 , (visibleIndex - 1))
} else if currentHorizontalOffset > rightThreshold || velocity.x > velocityThreshold {
visibleIndex += 1
}
var _proposedContentOffset = proposedContentOffset
_proposedContentOffset.x = CGFloat(collectionView!.bounds.width) * CGFloat(visibleIndex)
return _proposedContentOffset
}
}
And define the delegate methods in your view controller to insert a new card when the delegate tell it that it needs a new index
extension ViewController : CollectionViewFlowLayoutDelegate {
func flowLayout(flowLayout : CustomCollectionViewFlowLayout, insertIndex index : NSIndexPath) {
cards.append(Card())
collectionView.performBatchUpdates({
self.collectionView.insertItemsAtIndexPaths([index])
}) { (complete) in
}
}
And below is the custom Collection view that applies the animation while scrolling accordingly :)
class CollectionView : UICollectionView {
override var contentOffset: CGPoint {
didSet {
if self.tracking {
// When you are tracking the CustomCollectionViewFlowLayout does not update it's visible index until you let go
// So you should be adjusting the second to last cell on the screen
self.adjustTransitionForOffset(NSIndexPath(forRow: self.numberOfItemsInSection(0) - 1, inSection: 0))
} else {
// Once the CollectionView is not tracking, the CustomCollectionViewFlowLayout calls
// targetContentOffsetForProposedContentOffset(_:withScrollingVelocity:), and updates the visible index
// by adding 1, thus we need to continue the trasition on the second the last cell
self.adjustTransitionForOffset(NSIndexPath(forRow: self.numberOfItemsInSection(0) - 2, inSection: 0))
}
}
}
/**
This method applies the transform accordingly to the cell at a specified index
- parameter atIndex: index of the cell to adjust
*/
func adjustTransitionForOffset(atIndex : NSIndexPath) {
if let lastCell = self.cellForItemAtIndexPath(atIndex) as? CollectionViewCell {
let progress = 1.0 - (lastCell.frame.minX - self.contentOffset.x) / lastCell.frame.width
lastCell.cardView.alpha = progress
lastCell.cardView.layer.transform = CATransform3DScale(CATransform3DIdentity, progress, progress, 0.0)
}
}
}
I think u missed one point here in
let newIndexPath = NSIndexPath(forItem: addedCards.count, inSection: 0)
the indexPath should be
NSIndexPath(forItem : addedCards.count - 1, inSection : 0) not addedCards.count
Thats why you were getting the error
NSInternalInconsistencyException', reason: 'Invalid update: invalid number of items in section 0

UICollectionView cell reuse causing problems even after taking steps

I know this question has been asked many times.
I'm using a UICollectionView with custom cells which have a few properties, most important being an array of UISwitch's and and array of UILabel's. As you can guess, when I scroll, the labels overlap and the switches change state. I have implemented the method of UICollectionViewCell prepareForReuse in which I empty these arrays and reset the main label text.
I have tried to combine solutions from different answers and I have reached a point where my labels are preserved, but the state of my switches in the cells isn't. My next step was to create an array to preserve the state before removing the switches and then set the on property of a newly created switch to a value of this array at an index. This works, until after I scroll very fast and switches in cells which were not selected previously become selected(or unselected). This is creating a huge problem for me.
This is my collectionView cellForItemAtIndexPath method:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("pitanjeCell", forIndexPath: indexPath) as! PitanjeCell
// remove views previously created and create array to preserve the state of switches
var selectedSwitches: [Bool] = []
for item: UIView in cell.contentView.subviews {
if (item.isKindOfClass(UILabel) && !item.isEqual(cell.tekstPitanjaLabel)){
item.removeFromSuperview()
}
if (item.isKindOfClass(UISwitch)){
selectedSwitches.append((item as! UISwitch).on)
item.removeFromSuperview()
}
}
// get relevant data needed to place cells programmatically
cell.tekstPitanjaLabel.text = _pitanja[indexPath.row].getText()
let numberOfLines: CGFloat = CGFloat(cell.tekstPitanjaLabel.numberOfLines)
cell.setOdgovori(_pitanja[indexPath.row].getOdgovori() as! [Odgovor])
var currIndex: CGFloat = 1
let floatCount: CGFloat = CGFloat(_pitanja.count)
let switchConstant: CGFloat = 0.8
let switchWidth: CGFloat = cell.frame.size.width * 0.18
let heightConstant: CGFloat = (cell.frame.size.height / (floatCount + 2) + (numberOfLines * 4))
let labelWidth: CGFloat = cell.frame.size.width * 0.9
for item in _pitanja[indexPath.row].getOdgovori() {
// create a switch
let odgovorSwitch: UISwitch = UISwitch(frame: CGRectMake((self.view.frame.size.width - (switchWidth * 2)), currIndex * heightConstant , switchWidth, 10))
odgovorSwitch.transform = CGAffineTransformMakeScale(switchConstant, switchConstant)
let switchValue: Bool = selectedSwitches.count > 0 ? selectedSwitches[Int(currIndex) - 1] : false
odgovorSwitch.setOn(switchValue, animated: false)
// cast current item to relevant class
let obj: Odgovor = item as! Odgovor
// create a label
let odgovorLabel: UILabel = UILabel(frame: CGRectMake((self.view.frame.size.width / 12), currIndex * heightConstant , labelWidth, 20))
odgovorLabel.text = obj.getText();
odgovorLabel.lineBreakMode = NSLineBreakMode.ByWordWrapping
odgovorLabel.font = UIFont(name: (odgovorLabel.font?.fontName)!, size: 15)
// add to cell
cell.addSwitch(odgovorSwitch)
cell.addLabel(odgovorLabel)
currIndex++
}
return cell
}
My custom cell also implements methods addSwitch and addLabel which add the element to the contentView as a subview.
Is there any way I can consistently preserve the state of switches when scrolling?
EDIT: As per #Victor Sigler suggestion, I created a bydimensional array like this:
var _switchStates: [[Bool]]!
I initialized it like this:
let odgovori: Int = _pitanja[0].getOdgovori().count
_switchStates = [[Bool]](count: _pitanja.count, repeatedValue: [Bool](count: odgovori, repeatedValue: false))
And I changed my method like this:
for item: UIView in cell.contentView.subviews {
if (item.isKindOfClass(UILabel) && !item.isEqual(cell.tekstPitanjaLabel)){
item.removeFromSuperview()
}
if (item.isKindOfClass(UISwitch)){
_switchStates[indexPath.row][current] = (item as! UISwitch).on
current++
selectedSwitches.append((item as! UISwitch).on)
item.removeFromSuperview()
}
}
And in the end:
let switchValue: Bool = _switchStates.count > 0 ? _switchStates[indexPath.row][Int(currIndex) - 1] : false
First of all as you said in your question regarding the default behavior of the cell in the UICollectionView you need to save the state of each cell, in your case with the UISwitch, but you need to preserve the state in a local property, not in the cellForRowAtIndexPath method because this method is calles every time a cell is going to be reused.
So first declare the array where you going to save the state of the UISwitch's outside this function, something like this:
var selectedSwitches: [Bool] = []
Or you can declare it and then in your viewDidLoad instantiate it, it's up to you, I recommend you instantiate it in your viewDidLoad and only declare it as a property like this:
var selectedSwitches: [Bool]!
Then you can do whatever you want with the state of your UISwitch's always of course preserving when change to on or off.
I hope this help you.
I have done it!
In the end I implemented this:
func collectionView(collectionView: UICollectionView, didEndDisplayingCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath) {
var current: Int = 0
var cell = cell as! PitanjeCell
for item: UISwitch in cell.getOdgovoriButtons() {
if(current == _switchStates[0].count) { break;}
_switchStates[indexPath.row][current] = (item).on
current++
}
}
In this function I saved the state.
This in combo with prepareForReuse:
public override func prepareForReuse() {
self.tekstPitanjaLabel.text = nil
self._odgovoriButtons.removeAll()
self._odgovoriLabels.removeAll()
self._odgovori.removeAll()
super.prepareForReuse()
}
Finally did it!

Send Row and Section through Tag in Button Swift

I have this inside cellForRowAtIndexPath
cell.plusBut.tag = indexPath.row
cell.plusBut.addTarget(self, action: "plusHit:", forControlEvents: UIControlEvents.TouchUpInside)
and this function outside:
func plusHit(sender: UIButton!){
buildings[sender.tag].something = somethingElse
}
Is it possible to send the indexPath.row and indexPath.section, or some alternative??
Thanks!
EDIT
I approached it like this:
My Custom Button
class MyButton: UIButton{
var myRow: Int = 0
var mySection: Int = 0
}
My Custom Cell
class NewsCell: UITableViewCell{
#IBOutlet weak var greenLike: MyButton!
In CellForRowAtIndexPath
cell.greenLike.myRow = indexPath.row
I get an error on this line.
Is it possible to send the indexPath.row and indexPath.section, or some alternative??
If you impose a limit on the number of possible rows in a section, you can combine the two into a single number. For example, if you're willing to say that there will always be fewer than 1000 rows in a given section, you can use:
cell.plusBut.tag = (indexPath.section * 1000) + indexPath.row
and then use mod and division operators to recover:
row = sender.tag % 1000
section = sender.tag / 1000
Another possibility is to check the button's superview (and it's superview, etc.) until you find the cell. Once you have the cell, you can get the index path for that cell from the table.
A third option, perhaps the best one, is to have the button target the cell rather than some other object. The cell can then trigger an action in the view controller or other object using itself as sender.
You can create a subclass of UIButton and create an extra property section in it. And then you can use that class for cell button. You can do the same for row.
Here are few possible ways after subclassing UIButton
cell.plusBut.tag = indexPath.row
cell.plusBut.section = indexPath.section
Or
cell.plusBut.row = indexPath.row
cell.plusBut.section = indexPath.section
Or
cell.plusBut.indexPath = indexPath
Choose whatever suits you.
Here's a really easy solution I use:
in CellForRow:
button.tag = indexPath.row
cell.contentView.superview?.tag = indexPath.section
in the function retrieve it like this:
func buttonTapped(_ sender: Any) {
let button = sender as! UIButton
let row = button.tag
let sec = button.superview?.tag
//retrieve item like self.array[sec][row]
//do the rest of your stuff here
}
Swift 2.x - Extension with Associated Objects
private struct AssociatedKeys {
static var section = "section"
static var row = "row"
}
extension UIButton {
var section : Int {
get {
guard let number = objc_getAssociatedObject(self, &AssociatedKeys.section) as? Int else {
return -1
}
return number
}
set(value) {
objc_setAssociatedObject(self,&AssociatedKeys.section,Int(value),objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
var row : Int {
get {
guard let number = objc_getAssociatedObject(self, &AssociatedKeys.row) as? Int else {
return -1
}
return number
}
set(value) {
objc_setAssociatedObject(self,&AssociatedKeys.row,Int(value),objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
}
usage:
//set
cell.contextMenuBtn.section = indexPath.section
cell.contextMenuBtn.row = indexPath.row
//read
func showContextMenu (sender:UIButton) {
let track = dictionarySongs[sender.section][sender.row]
...
}
Send Row and Section through in Button you can use tag and accessibilityIdentifier, take a look:
Set value
button.tag = indexPath.row
button.accessibilityIdentifier = "\(indexPath.section)"
Get Value
let indexRow = button.tag
let indexSection = Int(button.accessibilityIdentifier!)
I suggest another approach. I don't like the subclass solution, I don't think the button should know its position.
Why not use a dictionary to translate the button into the IndexPath
// First initialize the lookup table
var buttonPositions = [UIButton: NSIndexPath]()
// add each button to the lookup table
let b = UIButton()
let path = NSIndexPath(forItem: 1, inSection: 1)
buttonPositions[b] = path
// Looking it up works like this
buttonPostions[activeButton]?.row
You can assign a custom tag for a UIButton via extension, take a look:
extension UIButton {
private struct ButtonTag {
static var tagKey = "key"
}
var myTag : (Int , Int)? {
set {
if let value = newValue {
objc_setAssociatedObject(self, &ButtonTag.tagKey, value as! AnyObject, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
get {
return objc_getAssociatedObject(self, &ButtonTag.tagKey) as? (Int , Int)
}
}
}

Resources