Struggling to understand moveItemAt with IGListDiff Swift - ios

I am using the IGListDiff part of the IGListKit
open source framework.
It enables me easier manipulation, action and update on my UICollectionView and UICollectionViewCell within my App.
But I am struggling to understand the implementation for the moveItemAt.
Indeed, I would like to be able to move my UICollectionViewCell from a long press on them. It appears that the IGListDiff can handle it, same as insert, delete and update.
Here is my implementation of the moveItemAt :
func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let item = items.remove(at: sourceIndexPath.item)
items.insert(item, at: destinationIndexPath.item)
}
Nothing special, without it I can't move my cells. But it triggers the update from IGListDiff too because I am modifying the datasource, and it feels like it "double" the update of my UICollectionView.
I am doing the remove/insert in the moveItemAt but also in my perform method when the dataSource is modified :
extension UICollectionView {
func perform(result: DiffList.Result) {
if result.hasChanges {
self.performBatchUpdates({
if !result.deletes.isEmpty {
self.deleteItems(at: result.deletes.compactMap { IndexPath(row: $0, section: 0) })
}
if !result.inserts.isEmpty {
self.insertItems(at: result.inserts.compactMap { IndexPath(row: $0, section: 0) })
}
if !result.updates.isEmpty {
self.reloadItems(at: result.updates.compactMap { IndexPath(row: $0, section: 0) })
}
if !result.moves.isEmpty {
result.moves.forEach({ (index) in
let toIndexPath = IndexPath(row: index.to, section: 0)
self.moveItem(at: IndexPath(row: index.from, section: 0), to: toIndexPath)
})
}
})
}
}
}
Any help from someone that has already implemented the moveItemAt with a DiffList algorithm would be appreciated.

Related

Swift iOS In table view Can I use reloadData after adding a row?

In table view
If use reloadData after adding a row, I think perfomence is not good
Is reloadData designed for that?
if not
How do I add a row?
FirstTime i Used reloadData()
now Used
let index = IndexPath(row: dataList.count - 1, section: 0)
tableView.storyTableView.insertRows(at: [index], with: .none)
but raise NSInternalInconsistencyException
So i will use reloadData again
Is there any problem if I keep using reloadData?
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if dataList.count < 1{
self.emptyTableLabel.layer.zPosition = 2
}else{
self.emptyTableLabel.alpha = 0
}
return dataList.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "DataCell", for: indexPath) as! Datacell
cell.parentTable = self
let data = self.dataList[indexPath.row]
cell.indexLabel.text = "\(indexPath.row+1)"
cell.nick.text = (data["UserNick"] as! String)
cell.content.text = (data["Content"] as! String)
cell.index = indexPath.row
cell.time.text = (data["RegisterDate"] as! String).calculDateTime()
return cell
}
Dynamically adds values ​​from the user to the table view,
It works well when data comes in one by one.
But An NSInternalInconsistencyException is thrown if multiple values ​​are received almost simultaneously.
Go through this Perform updates method .
func performBatchUpdates(_ updates: (() -> Void)?,
completion: ((Bool) -> Void)? = nil)
Perform the insertion here and Reload the Tableview after Completion
tableView.performBatchUpdates ({
let index = IndexPath(row: dataList.count - 1, section: 0)
tableView.storyTableView.insertRows(at: [index], with: .none)
},
completion: { (success) in
tableView.reloadData()
}
)
Check this one

Auto Scroll to Top

I was able to get it to auto scroll to the top upon opening the app, but now it won't let you scroll anymore because of the scrollView forcefully keeping it at the top due to willDisplay.
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if info.count > 0 {
collectionView.scrollToItem(at: IndexPath(item:info.count - 1, section: 0), at: .top, animated: false)
}
}
Where else can I put it?
Use the following code to achieve your needs. For animation, you just need to pass value true/false in animated of scrollToRow function.
Hope this will help you!
To scroll top without animation
func scrollToTopWithoutAnimation() {
DispatchQueue.main.async {
if self.dataArray.count > 0 {
let indexPath = IndexPath(row: 0, section: 0)
collectionView.scrollToItem(at: indexPath, at: .top, animated: false)
}
}
}
To scroll top with animation
func scrollToTopWithAnimation() {
DispatchQueue.main.async {
if self.dataArray.count > 0 {
let indexPath = IndexPath(row: 0, section: 0)
collectionView.scrollToItem(at: indexPath, at: .top, animated: true)
}
}
}
try this after collectionView reload
DispatchQueue.main.async {
self.collectionView.scrollsToTop = true
}

Swift inserting a cell in uicollectionview gives me an error

I have got this code
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
{
return comments.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = commentSection.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! CommentCell
return cell
}
And i have functions below this which inserts cells
DispatchQueue.main.async{
let indexPath = IndexPath(row: self.comments.count, section: 0)
self.commentSection.insertItems(at: [indexPath])
self.commentSection.reloadData()
}
But whenever i run this code it prints this error
libc++abi.dylib: terminating with uncaught exception of type NSException
And redirects me to AppDelegate file.Even though the error is very clearly written i can't understand the problem
You need to modify the data source first before inserting or deleting a cell from collectionView.
self.collectionView?.performBatchUpdates({
let indexPath = IndexPath(row: self.comments.count, section: 0)
comments.append(your_object) //add your object to data source first
self.collectionView?.insertItems(at: [indexPath])
}, completion: nil)
Make sure that you insert also additional object in your comments array before inserting items to collection view
performBatchUpdates is recommended way to insert items in collectionView. First update the data source and then call performBatchUpdates to insert a new item in collection view.
//Update DataSource
let newComment = "This is new comment"
comments.append(newComment)
let indexPath = IndexPath(item: self.comments.count - 1, section: 0)
var indexPaths: [IndexPath] = [indexPath]
// finally update the collection view
collectionView.performBatchUpdates({ () -> Void in
collectionView.insertItems(at: indexPaths)
}, completion: nil)
SWIFT 4
onMain {
CATransaction.begin()
CATransaction.setDisableActions(true)
self.chatData.append(res)
let indexPath = IndexPath(row: self.chatData.count - 1, section: 0)
self.collectionView?.insertItems(at: [indexPath])
CATransaction.commit()
print("scroll to index path: \(indexPath)")
self.collectionView.scrollToItem(at: indexPath, at: .bottom , animated: true)
//self.collectionView.scrollToMaxContentOffset(animated: true, newHeight: 100)
}

Move the row to bottom of table on selection?

I like to move the row to bottom of the table and persist it that is I like it to see my changes when I come to my screen again, if the eventStatus == 1 0r 2. I did the following but the rows are not moving
if eventSegCtrl.selectedSegmentIndex == 0 {
myEventCell.eventLabel.text = self.eventScheduleOnc[indexPath.row].eventNameOnc
let eventDesc = self.eventScheduleOnc[indexPath.row].eventDecOnc
let eventStatus = eventDesc?.eventStatus
if eventStatus == 1 {
myEventCell.eventLabel.textColor = UIColor.green
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .bottom, animated: true)
}
}
else if eventStatus == 2 {
myEventCell.eventLabel.textColor = UIColor.red
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .bottom, animated: true)
}
}
else {
myEventCell.eventLabel.textColor = UIColor.black
}
}
How do I resolve this?
You are scrolling to IndexPath(row: 0, section: 0) which means the first row of the first section, which means it will scroll to top.
If you only have 1 section, change this:
IndexPath(row: 0, section: 0)
into
let numberOfRows // How many rows you have?
IndexPath(row: numberOfRows - 1, section: 0)
If you have multiple sections and multiple rows, change into these instead:
let numberOfSections // How many sections you have
let numberOfRowsInLastSection = tableView.numberOfRows(inSection:numberOfSections-1)
IndexPath(row: numberOfRowsInLastSection-1, section: numberOfSections-1)
tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .bottom, animated: true)
does not move a single row, it tries to scroll the entire table view in a way that the row at the given index path (0,0) is located a the given position (bottom).
It sounds you would like to reorder the cells. In order to do so you have to:
adjust your data model.
either reload the entire table (this is not animated) or call
Swift:
func moveRow(at indexPath: IndexPath,
to newIndexPath: IndexPath)
If you wish to move row, you need to do this.
override func tableView(tableView: UITableView, canMoveRowAtIndexPath indexPath: NSIndexPath) -> Bool {
return true}
override func tableView(tableView: UITableView, moveRowAtIndexPath fromIndexPath: NSIndexPath, toIndexPath: NSIndexPath) {
var itemToMove = tableData[fromIndexPath.row]
tableData.removeAtIndex(fromIndexPath.row)
tableData.insert(itemToMove, atIndex: toIndexPath.row)
}

Swift: Unable to long-press drag cells to empty sections in UICollectionViewController and UITableView

Previously, I tried to replace the standard apple reorder controls (dragging cell from handle on right) with a long press drag in a UITableView. However, with the longpress drag I was for some reason unable to move cells to a section with no cells in it already. Now I am trying to implement a function where users can drag cells between 2 sections in a UICollectionViewController instead of a UITableView. I implemented the long-press drag function but I am having the same issue for some reason. How would I add a dummy cell to the sections so that they are never empty or is there a better way around this? Also is there a way to drag cells without having to longpress?
These are the functions I added to my UICollectionViewController class to enable the long-press drag:
override func viewDidLoad() {
super.viewDidLoad()
let longPressGesture = UILongPressGestureRecognizer(target: self, action: "handleLongGesture:")
self.collectionView!.addGestureRecognizer(longPressGesture)
}
func handleLongGesture(gesture: UILongPressGestureRecognizer) {
switch(gesture.state) {
case UIGestureRecognizerState.Began:
guard let selectedIndexPath = self.collectionView!.indexPathForItemAtPoint(gesture.locationInView(self.collectionView)) else {
break
}
collectionView!.beginInteractiveMovementForItemAtIndexPath(selectedIndexPath)
case UIGestureRecognizerState.Changed:
collectionView!.updateInteractiveMovementTargetPosition(gesture.locationInView(gesture.view!))
case UIGestureRecognizerState.Ended:
collectionView!.endInteractiveMovement()
default:
collectionView!.cancelInteractiveMovement()
}
}
override func collectionView(collectionView: UICollectionView, moveItemAtIndexPath sourceIndexPath: NSIndexPath, toIndexPath destinationIndexPath: NSIndexPath) {
let fromRow = sourceIndexPath.row
let toRow = destinationIndexPath.row
let fromSection = sourceIndexPath.section
let toSection = destinationIndexPath.section
var item: Item
if fromSection == 0 {
item = section1Items[fromRow]
section1Items.removeAtIndex(fromRow)
} else {
item = section2Items[sourceIndexPath.row]
section2Items.removeAtIndex(fromRow)
}
if toSection == 0 {
section1Items.insert(score, atIndex: toRow)
} else {
section2Items.insert(score, atIndex: toRow)
}
}
override func collectionView(collectionView: UICollectionView, canMoveItemAtIndexPath indexPath: NSIndexPath) -> Bool {
return true
}
Thanks
To answer the second part of your question first, use a UIPanGestureRecogniser rather than a UILongPressRecogniser.
Regarding empty sections, you can add a dummy cell, which has no visibility, to empty sections. Create a prototype cell in the storyboard with no subviews, and make sure it has the same background colour as the collection view.
You need to arrange that this cell is displayed if the section would otherwise be empty. But also you need to add a dummy cell when a move begins in a section which has only 1 cell, otherwise the section will collapse during the move and the user is unable to move the cell back into the section from which it started.
In the gesture handler, add a temporary cell when beginning a move. Also remove the cell when the drag completes if it has not already been removed (the delegate moveItemAtIndexPath method is not called if the cell does not actually move):
var temporaryDummyCellPath:NSIndexPath?
func handlePanGesture(gesture: UIPanGestureRecognizer) {
switch(gesture.state) {
case UIGestureRecognizerState.Began:
guard let selectedIndexPath = self.collectionView.indexPathForItemAtPoint(gesture.locationInView(self.collectionView)) else {
break
}
if model.numberOfPagesInSection(selectedIndexPath.section) == 1 {
// temporarily add a dummy cell to this section
temporaryDummyCellPath = NSIndexPath(forRow: 1, inSection: selectedIndexPath.section)
collectionView.insertItemsAtIndexPaths([temporaryDummyCellPath!])
}
collectionView.beginInteractiveMovementForItemAtIndexPath(selectedIndexPath)
case UIGestureRecognizerState.Changed:
collectionView.updateInteractiveMovementTargetPosition(gesture.locationInView(gesture.view!))
case UIGestureRecognizerState.Ended:
collectionView.endInteractiveMovement()
// remove dummy path if not already removed
if let dummyPath = self.temporaryDummyCellPath {
temporaryDummyCellPath = nil
collectionView.deleteItemsAtIndexPaths([dummyPath])
}
default:
collectionView.cancelInteractiveMovement()
// remove dummy path if not already removed
if let dummyPath = temporaryDummyCellPath {
temporaryDummyCellPath = nil
collectionView.deleteItemsAtIndexPaths([dummyPath])
}
}
}
In collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) always return 1 more than the number of items in your model, or an additional cell if a temporary dummy cell has been added.
func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
// special case: about to move a cell out of a section with only 1 item
// make sure to leave a dummy cell
if section == temporaryDummyCellPath?.section {
return 2
}
// always keep one item in each section for the dummy cell
return max(model.numberOfPagesInSection(section), 1)
}
In collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) allocate the dummy cell if the row is equal to the number of items in the model for that section.
Disable selection of the cell in collectionView(collectionView: UICollectionView, shouldSelectItemAtIndexPath indexPath: NSIndexPath) by returning false for the dummy cell and true for other cells.
Similarly disable movement of the cell in collectionView(collectionView: UICollectionView, canMoveItemAtIndexPath indexPath: NSIndexPath)
Make sure the target move path is limited to rows in your model:
func collectionView(collectionView: UICollectionView, targetIndexPathForMoveFromItemAtIndexPath originalIndexPath: NSIndexPath, toProposedIndexPath proposedIndexPath: NSIndexPath) -> NSIndexPath
{
let proposedSection = proposedIndexPath.section
if model.numberOfPagesInSection(proposedSection) == 0 {
return NSIndexPath(forRow: 0, inSection: proposedSection)
} else {
return proposedIndexPath
}
}
Now you need to handle the final move. Update your model, and then add or remove a dummy cell as required:
func collectionView(collectionView: UICollectionView, moveItemAtIndexPath sourceIndexPath: NSIndexPath, toIndexPath destinationIndexPath: NSIndexPath)
{
// move the page in the model
model.movePage(sourceIndexPath.section, fromPage: sourceIndexPath.row, toSection: destinationIndexPath.section, toPage: destinationIndexPath.row)
collectionView.performBatchUpdates({
// if original section is left with no pages, add a dummy cell or keep already added dummy cell
if self.model.numberOfPagesInSection(sourceIndexPath.section) == 0 {
if self.temporaryDummyCellPath == nil {
let dummyPath = NSIndexPath(forRow: 0, inSection: sourceIndexPath.section)
collectionView.insertItemsAtIndexPaths([dummyPath])
} else {
// just keep the temporary dummy we already created
self.temporaryDummyCellPath = nil
}
}
// if new section previously had no pages remove the dummy cell
if self.model.numberOfPagesInSection(destinationIndexPath.section) == 1 {
let dummyPath = NSIndexPath(forRow: 0, inSection: destinationIndexPath.section)
collectionView.deleteItemsAtIndexPaths([dummyPath])
}
}, completion: nil)
}
Finally make sure the dummy cell has no accessibility items so that it is skipped when voice over is on.

Resources