Collection View not reload every time - ios

I am using drag and drop functionality in collection view.its working fine but when I replace the values of array when user trap the cell into another position.I have reload the collection view.But when we drag and drop cell 0 and 1 collection view not reload all cell it reload only one cell.
I don't know what is the problem in my code.
Below is my code.
longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongGesture(gesture:)))
collection_view.addGestureRecognizer(longPressGesture)
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell_images", for: indexPath) as! AddImagesCollectionViewCell
cell.layer.borderWidth=1
if Details.Images[indexPath.item].sourceType == sourceType.URL
{
cell.img_images.downloadedFrom(link: Details.Images[indexPath.item].imageURL!, contentMode: .scaleAspectFill)
}
else
{
cell.img_images.image = Details.Images[indexPath.item].image
}
Details.Images[indexPath.row].sequence="\(indexPath.row + 1)"
return cell
}
func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool
{
if indexPath.row == Details.Images.count
{
return false
}
return true
}
func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath)
{
print("Starting Index: \(sourceIndexPath.item)")
print("Ending Index: \(destinationIndexPath.item)")
let tempArray = Details.Images[sourceIndexPath.item]
Details.Images.remove(at: sourceIndexPath.item)
Details.Images.insert(tempArray, at: destinationIndexPath.item)
collectionView.reloadData()
}
#objc func handleLongGesture(gesture: UILongPressGestureRecognizer)
{
switch(gesture.state)
{
case .began:
guard let selectedIndexPath = collection_view.indexPathForItem(at: gesture.location(in: collection_view))
else
{
break
}
if !(selectedIndexPath.row == Details.Images.count)
{
collection_view.beginInteractiveMovementForItem(at: selectedIndexPath)
}
case .changed:
guard let selectedIndexPath = collection_view.indexPathForItem(at: gesture.location(in: collection_view))
else
{
break
}
if !(selectedIndexPath.row == Details.Images.count)
{
collection_view.updateInteractiveMovementTargetPosition(gesture.location(in: gesture.view!))
}
case .ended:
collection_view.endInteractiveMovement()
default:
collection_view.cancelInteractiveMovement()
}
}

I have same issue. Don't know why reloadData() isn't working after endInteractiveMovement() or cancelInteractiveMovement(), but I have a workaround for this.
Instead of using reloadData() use reloadSections(), like:
self.collectionView.cancelInteractiveMovement()
// Not working: self.collectionView.reloadData()
UIView.performWithoutAnimation {
self.collectionView.reloadSections(IndexSet(integer: 0))
}
Also if you use reloadData() in DispatchQueue.main.asyncAfter() it will work.

There is no bugs in your code, collectionView will reload only one cell (one you have draged).If you want to reload all collection view you need to call reloadData() after endInteractiveMovement().

I had this problem with reordering via drag-and-drop - the collection view won't reload all the cells whose positions change because of the move operation. I fixed it by calling moveItemAtIndexPath:toIndexPath: in my implementation of performDropWithCoordinator. See this question.

Related

UICollectionViewCell created from XIB will cause flickering during drag and drop

I implement a simple drag and drop sample.
import UIKit
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
private var collectionView: UICollectionView?
var colors: [UIColor] = [
.link,
.systemGreen,
.systemBlue,
.red,
.systemOrange,
.black,
.systemPurple,
.systemYellow,
.systemPink,
.link,
.systemGreen,
.systemBlue,
.red,
.systemOrange,
.black,
.systemPurple,
.systemYellow,
.systemPink
]
override func viewDidLoad() {
super.viewDidLoad()
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.itemSize = CGSize(width: view.frame.size.width/3.2, height: view.frame.size.width/3.2)
layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
//collectionView?.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
let customCollectionViewCellNib = CustomCollectionViewCell.getUINib()
collectionView?.register(customCollectionViewCellNib, forCellWithReuseIdentifier: "cell")
collectionView?.delegate = self
collectionView?.dataSource = self
collectionView?.backgroundColor = .white
view.addSubview(collectionView!)
let gesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPressGesture))
collectionView?.addGestureRecognizer(gesture)
}
#objc func handleLongPressGesture(_ gesture: UILongPressGestureRecognizer) {
guard let collectionView = collectionView else {
return
}
switch gesture.state {
case .began:
guard let targetIndexPath = collectionView.indexPathForItem(at: gesture.location(in: self.collectionView)) else {
return
}
collectionView.beginInteractiveMovementForItem(at: targetIndexPath)
case .changed:
collectionView.updateInteractiveMovementTargetPosition(gesture.location(in: collectionView))
case .ended:
collectionView.endInteractiveMovement()
default:
collectionView.cancelInteractiveMovement()
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
collectionView?.frame = view.bounds
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return colors.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
cell.backgroundColor = colors[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.size.width/3.2, height: view.frame.size.width/3.2)
}
func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool {
return true
}
func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let item = colors.remove(at: sourceIndexPath.row)
colors.insert(item, at: destinationIndexPath.row)
}
}
However, I notice that, if my UICollectionViewCell is created with XIB, it will randomly exhibit flickering behaviour, during drag and drop.
The CustomCollectionViewCell is a pretty straightforward code.
CustomCollectionViewCell.swift
import UIKit
extension UIView {
static func instanceFromNib() -> Self {
return getUINib().instantiate(withOwner: self, options: nil)[0] as! Self
}
static func getUINib() -> UINib {
return UINib(nibName: String(describing: self), bundle: nil)
}
}
class CustomCollectionViewCell: UICollectionViewCell {
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
}
Flickering
By using the following code
let customCollectionViewCellNib = CustomCollectionViewCell.getUINib()
collectionView?.register(customCollectionViewCellNib, forCellWithReuseIdentifier: "cell")
It will have the following random flickering behaviour - https://youtu.be/CbcUAHlRJKI
No flickering
However, if the following code is used instead
collectionView?.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
Things work fine. There are no flickering behaviour - https://youtu.be/QkV2HlIrXK8
May I know why it is so? How can I avoid the flickering behaviour, when my custom UICollectionView is created from XIB?
Please note that, the flickering behaviour doesn't happen all the time. It happens randomly. It is easier to reproduce the problem using real iPhone device, than simulator.
Here's the complete sample code - https://github.com/yccheok/xib-view-cell-cause-flickering
While we are rearranging cells in UICollectionView (gesture is active), it handles all of the cell movements for us (without having us to worry about changing dataSource while the rearrange is in flight).
At the end of this rearrange gesture, UICollectionView rightfully expects that we will reflect the change in our dataSource as well which you are doing correctly here.
func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let item = colors.remove(at: sourceIndexPath.row)
colors.insert(item, at: destinationIndexPath.row)
}
Since UICollectionView expects a dataSource update from our side, it performs following steps -
Call our collectionView(_:, moveItemAt:, to:) implementation to provide us a chance to reflect the changes in dataSource.
Call our collectionView(_:, cellForItemAt:) implementation for the destinationIndexPath value from call #1, to re-create a new cell at that indexPath from scratch.
Okay, but why would it perform step 2 even if this is the correct cell to be at that indexPath?
It's because UICollectionView doesn't know for sure whether you actually made those dataSource changes or not. What happens if you don't make those changes? - now your dataSource & UI are out of sync.
In order to make sure that your dataSource changes are correctly reflected in the UI, it has to do this step.
Now when the cell is being re-created, you sometimes see the flicker. Let the UI reload the first time, put a breakpoint in the cellForItemAt: implementation at the first line and rearrange a cell. Right after rearrange completes, your program will pause at that breakpoint and you can see following on the screen.
Why does it not happen with UICollectionViewCell class (not XIB)?
It does (as noted by others) - it's less frequent. Using the above steps by putting a breakpoint, you can catch it in that state.
How to solve this?
Get a reference to the cell that's currently being dragged.
Return this instance from cellForItemAt: implementation.
var currentlyBeingDraggedCell: UICollectionViewCell?
var willRecreateCellAtDraggedIndexPath: Bool = false
#objc func handleLongPressGesture(_ gesture: UILongPressGestureRecognizer) {
guard let cv = collectionView else { return }
let location = gesture.location(in: cv)
switch gesture.state {
case .began:
guard let targetIndexPath = cv.indexPathForItem(at: location) else { return }
currentlyBeingDraggedCell = cv.cellForItem(at: targetIndexPath)
cv.beginInteractiveMovementForItem(at: targetIndexPath)
case .changed:
cv.updateInteractiveMovementTargetPosition(location)
case .ended:
willRecreateCellAtDraggedIndexPath = true
cv.endInteractiveMovement()
default:
cv.cancelInteractiveMovement()
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if willRecreateCellAtDraggedIndexPath,
let currentlyBeingDraggedCell = currentlyBeingDraggedCell {
self.willRecreateCellAtDraggedIndexPath = false
self.currentlyBeingDraggedCell = nil
return currentlyBeingDraggedCell
}
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
cell.contentView.backgroundColor = colors[indexPath.item]
return cell
}
Will this solve the problem 100%?
NO. UICollectionView will still remove the cell from it's view hierarchy and ask us for a new cell - we are just providing it with an existing cell instance (that we know is going to be correct according to our own implementation).
You can still catch it in the state where it disappears from UI before appearing again. However this time there's almost no work to be done, so it will be significantly faster and you will see the flickering less often.
BONUS
iOS 15 seems to be working on similar problems via UICollectionView.reconfigureItems APIs. See an explanation in following Twitter thread.
Whether these improvements will land in rearrange or not, we will have to see.
Other Observations
Your UICollectionViewCell subclass' XIB looks like following
However it should look like following (1st one is missing contentView wrapper, you get this by default when you drag a Collection View Cell to the XIB from the View library OR create a UICollectionViewCell subclass with XIB).
And your implementation uses -
cell.backgroundColor = colors[indexPath.row]
You should use contentView to do all the UI customization, also note the indexPath.item(vs row) that better fits with cellForItemAt: terminology (There are no differences in these values though). cellForRowAt: & indexPath.row are more suited for UITableView instances.
cell.contentView.backgroundColor = colors[indexPath.item]
UPDATE
Should I use this workaround for my app in production?
NO.
As noted by OP in the comments below -
The proposed workaround has 2 shortcomings.
(1) Missing cell
(2) Wrong content cell.
This is clearly visible in https://www.youtube.com/watch?v=uDRgo0Jczuw Even if you perform explicit currentlyBeingDraggedCell.backgroundColor = colors[indexPath.item] within if block, wrong content cell issue is still there.
The flickering is caused by the cell being recreated at its new position. You can try holding to the cell.
(only the relevant code is shown)
// keeps a reference to the cell being dragged
private weak var draggedCell: UICollectionViewCell?
// the flag is set when the dragging completes
private var didInteractiveMovementEnd = false
#objc func handleLongPressGesture(_ gesture: UILongPressGestureRecognizer) {
switch gesture.state {
case .began:
// keep cell reference
draggedCell = collectionView.cellForItem(at: targetIndexPath)
collectionView.beginInteractiveMovementForItem(at: targetIndexPath)
case .ended:
// reuse the cell in `cellForItem`
didInteractiveMovementEnd = true
collectionView.performBatchUpdates {
collectionView.endInteractiveMovement()
} completion: { completed in
self.draggedCell = nil
self.didInteractiveMovementEnd = false
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
// reuse the dragged cell
if didInteractiveMovementEnd, let draggedCell = draggedCell {
return draggedCell
}
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
...
}

Auto Select Middle Visible Cell Of Collection View

I'm trying to select and highlight the middle cell of the visible cells in a collection view at any given time. The collection view in question displays days for six months forwards and back.
I've tried using the scroll view delegates and the collection view delegates. But all that works is select and highlight code in didSelectItem() collection view delegate.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("delegate called")
collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
collectionView.cellForItem(at: indexPath)?.backgroundColor = UIColor.highlightCellGreen()
if let cell = collectionView.cellForItem(at: indexPath) as? ClientListDateCollectionViewCell{
monthLabel.text = cell.monthName
monthLabel.text = monthLabel.text?.capitalized
}
I tried to select the middle cell while scrolling using the viewDidScroll() delegate. But, I wasn't able to get the output I wanted.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let visibleCellCount = dateCollectionView.indexPathsForVisibleItems.count
let cellCount = dateCollectionView.visibleCells.count
let visibleCells = dateCollectionView.indexPathsForVisibleItems[visibleCellCount-1/2]
if visibleCellCount>0{
let middle = visibleCellCount/2
let midValue = dateCollectionView.indexPathsForVisibleItems[middle]
dateCollectionView.selectItem(at: midValue, animated: true, scrollPosition: .centeredHorizontally)
}
How do I go about selecting the middle cell?
edit 1: The collection view starts on the leftmost point and then scrolls to the middle i.e, today's date
You can use delegate of UICollectionView (i.e: didHighlightItemAtIndexPath). just make sure to call collection view delegates on your desired time by calling reload function
self.collectionView.reloadData()
and in you collection view delegate just do this
func collectionView(collectionView: UICollectionView, didHighlightItemAtIndexPath indexPath: NSIndexPath){
var cell : UICollectionViewCell = UICollectionViewCell()
self.collectionView.cellForItemAtIndexPath = indexPath
//change highlighted color as of your need
cell.view.backgroundColor = UIColor.init(red: 25, green: 118, blue: 210).cgColor
}
This will highlight you selected item
Disable multiple selection (or selection entirely?) to make things easier.
collectionView.allowsMultipleSelection = false
On scrollViewDidScroll(_:) get the center point of the screen as CGpoint.
let center = collectionView.center
Use that information to get the index path of the center item
let indexPath = collectionView.indexPathForItem(at: center)
Select the item
collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .top)
Suppose that you have the horizontal of displaying, and you want to have the auto scroll to the center of your item in datasource.
Creating a method and calling it immediately after your collection view is completely configured:
func scrollToCenterIndex() {
let centerIndex = LIST_OF_YOUR_DATA_SOURCE.count / 2
let indexPath = IndexPath(item: centerIndex, section: 0)
self.collectionView.scrollToItem(at: indexPath,
at: .right,
animated: false)
}
Inside the method:
public func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CELL,
for: indexPath) as? CustomCell else {
fatalError("Cannot create cell")
}
If indexPath.row == LIST_OF_YOUR_DATA_SOURCE.count / 2 {
// perform your hight light color to the cell
} else {
// reset your hight light color to default color
}
let model = LIST_OF_YOUR_DATA_SOURCE[indexPath.row]
cell.configure(model)
return cell
}
I think you can use a method to get the center point of collection view, and use this value to get the the middle of visible cell.
let centerPoint = self.view.convert(collectionView.center, to: collection)
Here is an example I did it with a tableView. You can apply it to your collection view with the same approach.
class ViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
var dataSource = Array(1...31)
var centerIndex: IndexPath?
func setCellSelected(cell: UITableViewCell, _ selected: Bool) {
cell.contentView.backgroundColor = selected ? .green : .white
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
dataSource.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CELL")
cell?.textLabel?.text = String(dataSource[indexPath.row])
let center = self.view.convert(tableView.center, to: tableView)
if let index = tableView.indexPathForRow(at: center), let cell = cell {
setCellSelected(cell: cell, indexPath.row == index.row)
}
return cell!
}
}
extension ViewController: UITableViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// reset the previous hight light cell
if let centerIndex = centerIndex, let cell = tableView.cellForRow(at: centerIndex) {
setCellSelected(cell: cell, false)
}
// set hight light to a new center cell
let center = self.view.convert(tableView.center, to: tableView)
if let index = tableView.indexPathForRow(at: center), let cell = tableView.cellForRow(at: index) {
setCellSelected(cell: cell, true)
centerIndex = index
}
}
}
I was also trying to do the auto-selection of the middle visible cell of the collection view, and I got the solution, here is the solution:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
// Reload Collection View
collectionView.reloadData()
// Find centre point of collection view
let visiblePoint = CGPoint(x: collectionView.center.x + collectionView.contentOffset.x, y: collectionView.center.y + collectionView.contentOffset.y)
// Find index path using centre point
guard let newIndexPath = collectionView.indexPathForItem(at: visiblePoint) else { return }
// Select the new centre item
collectionView.selectItem(at: newIndexPath, animated: true, scrollPosition: .centeredHorizontally) }
You need to use the Scroll view delegate function, scrollViewDidEndDecelerating. Reload the collection view first. Second, find the center visible point of the collection view. Third, using the center visible point, find the indexPath of collection view and finally use the index to select the item in the collection view.
I know I answered this question a little late, still thinking that it will be helpful for someone.
Cheers!

How can I change the properties of a cell that is not yet on the screen?

I've an app that consists of a timetable (UITableView) and a daybar (UICollectionView). The user can change their selected day by one of two methods:
Select a day from the DayBar
Click the "JumpToTomorrow" Button at the bottom of the table view.
The problem with 2. right now is that when I click it in a scenario where the next day has not yet been scrolled on to the screen, the cell for that day is not selected (see below).
DayCollectionViewCell.isSelected (not being called for some reason)
override var isSelected: Bool {
didSet {
self.backgroundColor = self.getBackgroundColor(selected: isSelected)
self.layer.borderColor = self.getBorderColor(selected: isSelected)
self.number.attributedText = NSAttributedString(string: number.text!, attributes: self.getTextAttributes(selected: isSelected))
}
}
DayBarController Functions
override func viewDidLoad() {
super.viewDidLoad()
// To preserve selection between presentations
self.clearsSelectionOnViewWillAppear = false
setEdgeInsets()
BroadcastManager.listen(to: PrepNotifications.JumpToTomorrowClicked, listenerId: "UpdateSelectedDay", react: updateSelectedDay)
}
/// change the selected day and notify the app about the change
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
PrepGlobals.Calendar.SELECTED_DAY = indexPath.row
BroadcastManager.broadcast(PrepNotifications.SelectedDayChanged)
//scroll to center on selected day in case of clicked on today
if (PrepGlobals.Calendar.TODAY_INDEX == indexPath.row){
self.collectionView?.scrollToItem(
at: indexPath,
at: .centeredHorizontally,
animated: true
)
}
}
private func updateSelectedDay(_ userInfo: [AnyHashable: Any]? = nil) {
PrepGlobals.Calendar.SELECTED_DAY += 1
let selectedDayIndex = IndexPath(row: PrepGlobals.Calendar.SELECTED_DAY, section: 0)
self.collectionView!.selectItem(at: selectedDayIndex, animated: true, scrollPosition: .right)
BroadcastManager.broadcast(PrepNotifications.SelectedDayChanged)
}
I edited my answer. Actually you have to set up isSelected manually.
override func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
cell.isSelected = (indexPath.row == PrepGlobals.Calendar.SELECTED_DAY)
}
You should modify your collectionView(_:cellForItemAt:) to check the cell index should be selected or not and update the style there.
Disabling Pre-fetching fixed this.

CollectionView Cell Moving when Selected

I have a CollectionView issue, I have a video showing the problem detailed below. When I click one cell it moves in a weird manner.
Here is my code:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
selectedFilter = indexPath.row
if filters[indexPath.row] != "Todo" {
filteredNews = news.filter { $0.category == filters[indexPath.row] }
} else {
filteredNews = news
}
tableView.reloadData()
collectionView.reloadData()
}
My Cell is moving, (Just the last cell, don't know why).
I think it might be related to collectionView.reloadData() But I need to do that for updating the green bar you can see on this Video when I select a Cell.
How can I make it not move? Someone had had a similar problem?
I noticed you reloaded a tableView during collectionView didSelectItemAt. If that tableView is a superView of your collectionView that will be the exact reason why you are having this abnormal behaviour.
If it were not, I can offer 3 solutions:
This library have a view controller subclass that can create the effect you want to show.
Manually create a UIView/UIImageView that is not inside the collectionView but update it's position during the collectionView's didSelectItemAt delegate method to but visually over the cell instead - this would require some calculation, but your collectionView will not need to reload.
You can attempt to only reload the two affected cells using the collectionView's reloadItem(at: IndexPath) method.
Know that when you reload a table/collection view, it will not change the current visible cell. However any content in each cell will be affected.
Finally I Solve it! I removed collectionView.reloadData() and added my code to change colors inside didSelectItemAt changing current selected item and old selected item (I created a Variable to see which one was the old selected item).
If someone interested, here is my code:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let oldSelectedFilter = selectedFilter
if selectedFilter != indexPath.row {
let oldIndexPath = IndexPath(item: oldSelectedFilter, section: 0)
selectedFilter = indexPath.row
if filters[indexPath.row] != "Todo" {
filteredNews = news.filter { $0.category == filters[indexPath.row] }
} else {
filteredNews = news
}
if let cell = collectionView.cellForItem(at: indexPath) as? FiltersCollectionViewCell {
cell.selectedView.backgroundColor = MainColor
}
if let cell = collectionView.cellForItem(at: oldIndexPath) as? FiltersCollectionViewCell {
cell.selectedView.backgroundColor = UIColor(red:0.31, green:0.33, blue:0.35, alpha:1.0)
}
tableView.reloadData()
}
}

UICollectionView attempts to dequeue cells beyond data source bounds while reordering is in progress

I have implemented collection view with cell reordering
class SecondViewController: UIViewController {
#IBOutlet weak var collectionView: UICollectionView!
private var numbers: [[Int]] = [[], [], []]
private var longPressGesture: UILongPressGestureRecognizer!
override func viewDidLoad() {
super.viewDidLoad()
for j in 0...2 {
for i in 0...5 {
numbers[j].append(i)
}
}
longPressGesture = UILongPressGestureRecognizer(target: self, action: "handleLongGesture:")
self.collectionView.addGestureRecognizer(longPressGesture)
}
func handleLongGesture(gesture: UILongPressGestureRecognizer) {
switch(gesture.state) {
case .Began:
guard let selectedIndexPath = self.collectionView.indexPathForItemAtPoint(gesture.locationInView(self.collectionView)) else {
break
}
collectionView.beginInteractiveMovementForItemAtIndexPath(selectedIndexPath)
case .Changed:
collectionView.updateInteractiveMovementTargetPosition(gesture.locationInView(gesture.view!))
case .Ended:
collectionView.endInteractiveMovement()
default:
collectionView.cancelInteractiveMovement()
}
}
}
extension SecondViewController: UICollectionViewDataSource {
func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
return numbers.count
}
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
let number = numbers[section].count
print("numberOfItemsInSection \(section): \(number)")
return number
}
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
print("cellForItemAtIndexPath: {\(indexPath.section)-\(indexPath.item)}")
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("Cell", forIndexPath: indexPath) as! TextCollectionViewCell
cell.textLabel.text = "\(numbers[indexPath.section][indexPath.item])"
return cell
}
func collectionView(collectionView: UICollectionView, moveItemAtIndexPath sourceIndexPath: NSIndexPath, toIndexPath destinationIndexPath: NSIndexPath) {
let temp = numbers[sourceIndexPath.section].removeAtIndex(sourceIndexPath.item)
numbers[destinationIndexPath.section].insert(temp, atIndex: destinationIndexPath.item)
}
}
It works fine until I try to drag an item from section 0 to section 2 (which is off screen). When I drag the item to the bottom of collection view, it slowly starts scrolling down. At some point (when it scrolls past section 1) application crashes with fatal error: Index out of range because it attempts to request cell at index 6 in section 1 while there are only 6 items in this section. If I try to drag an item from section 1 to section 2, everything works fine.
Here's an example project reproducing this problem (credit to NSHint):
https://github.com/deville/uicollectionview-reordering
Is this a bug in framework or am I missing something? If it is the former, what would be the workaround?
I actually ran into this issue today. Ultimately the problem was in itemAtIndexPath - I was referencing the datasource to grab some properties for my cell : thus the source of the out of bounds crash. The fix for me was to keep a reference to the currently dragging cell and in itemAtIndexPath; check the section's datasource length versus the passed NSIndexPath and if out of bounds; refer to the dragging cell's property.
Likesuchas (in Obj-C ... haven't moved to Swift yet) :
NSArray* sectionData = [collectionViewData objectAtIndex:indexPath.section];
MyObject* object;
if (indexPath.row < [sectionData count]) {
object = [dataObject objectAtIndex:indexPath.row];
} else {
object = draggingCell.object;
}
[cell configureCell:object];
Not sure if this is similar to your exact issue; but it was mine as everything is working as expected now. :)

Resources