I have a UICollectionView that uses a flow layout. It has two sections. Based on some user input, I move cells from the first section to the second. To animate this change I use the following code:
let oldIndexPath = self.indexPathForAsset(asset)
self.collectionView.performBatchUpdates({
//change data source
let newIndexPath = self.indexPathForAsset(asset)
self.collectionView.moveItem(at: oldIndexPath, to: newIndexPath)
if self.numberOfCollectionViewSections() == 1 {
self.collectionView.deleteSections([0])
}
}, completion: nil)
the cells animate fine from the first second to the second, except for the last one. When the last cell is moved, I delete the section, and deleteSections is not animated. Also, if I pass a completion block to performBatchUpdates, it is not called.
How can I animate the move of the last cell to the second section and the deletion of the first section?
UIView.animate(withDuration: 4) {
self.collectionView.moveItem(at: oldIndexPath, to: newIndexPath)
}
try doing this
Related
I have custom layout with fullscreen cells. When removing cell from the left (it's not visible at the time), UICollectionView jumps to the next cell.
It's like current cell was at index 4 and when cell on the left removed the next cell has index 4 now and immediately scroll to the next cell.
Describing in 3 steps (A is cell that need to be fullscreen, x will be removed, o other cells, large letter is fullscreen):
ooooAoo
oooxAoo
oooaOo
But must keep this oooAoo
Here is my solution if it can help to anybody, did not found how to achieve desired offset natively, so just scrolling contentOffset to the desired position right after reloadData():
var currentCell: MyCollectionViewCell? {
return (visibleCells.sorted { $0.frame.width > $1.frame.width }.first) as? MyCollectionViewCell
}
//-----------------------------------------
//some model manipulating code, removing desired items here...
let currentList = currentCell?.parentList
reloadData()
if let list = currentList, let index = self.lists.firstIndex(of: list) {
self.scrollToItem(at: IndexPath(row: index, section: 0), at: .centeredHorizontally, animated: false)
}
currentCell is computed property, returns optional middle cell.
Detect which cell is the largest, because of custom flowlayout logic.
parentList is the model item, I can compare cell by it to make life
easier. I check which list was attached to the cell before
reloadData().
I have a collectionView and allow a user to dynamically add cells to the collectionView. The view controller only shows one cell at a time and I want the first textView (which is part of the cell) to become the firstResponder (and keep the keyboard visible at all times), which works fine when loading the view controller (as well as in one of the cases below).
I have created a method to detect the current cell, which I call every time in any of these cases: (1) user scrolls from one cell to another (method placed in scrollViewWillEndDragging), (2) user taps UIButtons to navigate from one cell to another, (3) user taps UIButton to create and append a new cell at the end of the array (which is used by the collectionView).
This is the method:
func setNewFirstResponder() {
let currentIndex = IndexPath(item: currentCardIndex, section: 0)
if let newCell = collectionView.cellForItem(at: currentIndex) as? AddCardCell {
newCell.questionTextView.becomeFirstResponder()
}
}
Now my problem is that this only works in case (1). Apparently I have no cell of type AddCardCell in cases (2) and (3). When I print the currentCardIndex, I get the same result, in all of the cases, which is very confusing.
Any hints why I wouldn't be able to get the cell yet in cases 2 and 3, but I am in case 1?
As a reference here are some of the methods that I am using:
//Update index and labels based on user swiping through card cells
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
//Update index based on targetContentOffset
let x = targetContentOffset.pointee.x
currentCardIndex = Int(x / view.frame.width)
setNewFirstResponder()
}
And the other method, from which it doesn't work (case 3):
//Method to add a new cell at end of collectionView
#objc func handleAddCell() {
//Inserting a new index path into tableView
let newIndexPath = IndexPath(item: autoSaveCards.count - 1, section: 0)
collectionView.insertItems(at: [newIndexPath])
collectionView.scrollToItem(at: newIndexPath, at: .left, animated: true)
currentCardIndex = autoSaveCards.count - 1
setNewFirstResponder()
}
Regarding case 2,3 i think that the cell is not yet loaded so if let fails , you can try to dispatch that after some time like this , also general note if the cell is not visible then it's nil
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
setNewFirstResponder()
}
Also it can work if you set animated:false to scrollToItem
I have a set of cells in the same section. When I press a button on one of the cells, I would like it to move to the bottom, with an animation, moving over the other cells. The animation I have in mind is similar to what happens when you drag a cell around in editing mode, but I would like it to be done programmatically when I tap on a button.
For what I have gathered, I should use tableView.moveRowAtIndexPath, but I can't seem to do it properly:
//inside the function for when the button is pressed
let cell = sender.superview?.superview?.superview as! MainTableViewCell
let indexPath = tableView.indexPathForCell(cell)
let goal = goals[(indexPath?.row)!]
goals.removeAtIndex((indexPath?.row)!)
goals.append(goal)
tableView.beginUpdates()
self.tableView.moveRowAtIndexPath(indexPath!, toIndexPath: indexPaths.last!)
tableView.endUpdates()
tableView.reloadData()
After this, the function disables the button that triggers the function itself. What I have noticed happening is that not only does the cell that I would like to move get disabled, but the last cell in the section also does. If the last cell is already disabled, then the cell before last gets disabled and so on.
Any help would be greatly appreciated.
EDIT: I also call canMoveRowAtIndexPath and return true.
If you call tableView.reloadData() your will lose you animation effect. You should just be able to move the cell, then update your data source (the goals array) to reflect the new order of the items. You also do not need to call tableView.beginUpdates() and tableView.endUpdates() for a single update. You would use those calls to batch multiple updates (inserts, deletes, moves, etc.)
//inside the function for when the button is pressed
let cell = sender.superview?.superview?.superview as! MainTableViewCell
let indexPath = tableView.indexPathForCell(cell)
let goal = goals[(indexPath?.row)!]
tableView.moveRowAtIndexPath(indexPath!, toIndexPath: indexPaths.last!)
goals.removeAtIndex((indexPath?.row)!)
goals.append(goal)
I created a simple test project to verify, using simple strings as the data items, and the animation effect worked as expected. See gist here: MoveTableRow
I do this so:
UIView.transition(with: tableView, duration: 0.35, options: .transitionCrossDissolve, animations: { self.tableView.moveRow(at: indexPath, to: indexPath.last) })
For move to the beginning:
UIView.transition(with: tableView, duration: 0.35, options: .transitionCrossDissolve, animations: { self.tableView.moveRow(at: indexPath, to: [0,0]) })
I'm trying to simulate a Whatsapp Chat any cell will have an image (for tail of the bubble), a bubble which is just View with color and some corner radius and a label which will represent the text of the message.
I've put a print before and after the call
self.messagesTableView.reloadData()
Once the after print is called tableView keeps some time doint I don't know what till the data is shown. And same happens with Insert row at indexpath, it takes some time till show the insert animation.
func displayMessages(viewModel: GroupChatMessages.GetChatMessages.ViewModel) {
let displayedMessage = viewModel.displayedMessages
print ("i'm here!")
messages = displayedMessage!
//self.messagesTableView.performSelectorOnMainThread(Selector("reloadData"), withObject: nil, waitUntilDone: true)
self.messagesTableView.reloadData()
print ("i'm here2!")
firstTime = false
self.setVisible(hiddenTableView: false, hiddenChatLoader: true)
self.scrollToLastMessage(false)
self.messagesLoaded = true
}
I've tried to do dispatched with queue, and the commented line before reloadData(), but nothings works and nothing represent a significative time.
Maybe could be for the image of the bubble? I don't know. I have this image saved on Assets, so I'm not downloading it from internet.
self.setVisible just hide the loader and show the tableView but I've tried too moving it up and nothings changes. Any further information you need let me know. Thanks!
EDIT:
Well I've seen that the problem comes from the scroll to last cell, this is where it takes the major part of the time.
func scrollToLastMessage(animated: Bool) {
let section = 0
let lastItemIndex = self.messagesTableView.numberOfRowsInSection(section) - 1
let indexPath:NSIndexPath = NSIndexPath.init(forItem: lastItemIndex, inSection: section)
self.messagesTableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: .Bottom, animated: animated)
self.scrollDownButton.hidden = true
}
There is a posibility to optimize that scroll, because I have to do a Scroll because once the data is loaded, the first I've see is the top row of the tableView, but I would like to see the bottom one (last). Thanks!
methods like reloadData() should be considered as UI methods and it's mandatory to call them in main thread:
DispatchQueue.main.async { tableView.reloadData() }
It's better not to use reloadData() function unless a significant amount of cells need to refresh or data source has been changed instead use this method to add new rows:
tableView.insertRows(at: [IndexPath], with: UITableViewRowAnimation)
and for refreshing cell:
tableView.reloadRows(at: [IndexPath], with: UITableViewRowAnimation)
also if the cell has a considerable amount of images and rendering, use this code to make scrolling faster:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath)
// ADD THESE TWO LINE
cell.layer.shouldRasterize = true
cell.layer.rasterizationScale = UIScreen.main.scale
}
Using these ways will boost loading speed significantly
Finally the solution that I've found to avoid dying while waiting scrolling to last element any single time, is swapping orientation of table
tableView.transform = CGAffineTransformMakeRotation(-M_PI);
cell.transform = CGAffineTransformMakeRotation(M_PI);
Now headerView and footerView are reversed. For exemple, if you would like insert rows at (visually) at the bottom of the TableView with this configuration you should add it at position 0 forRow: 0 atSection: "WhereYouAre". This way when you add new element, no scroll is needed, because scroll is automatically. Amazing and strange answer IMHO.
I've found this solution here:
Solution Link
#Christos Hadjikyriacou solved there.
I want to make next:
I have an UITableView that have to dispaly words (A-Z).
Currently when view did load I have one cell that displayed (and this is correct). First cell display first word from my array words.
Purpose:
I want to move to the cell that must display 10 word from my array, but problem is that the cell with indexPath.row = 10 does not exist (and this correct, because I don't scroll yet).
What is a right wait to make transition from 1 to 10 cell.
I think if I don't use dequeueReusableCellWithIdentifier for creating my cell I can do it and solve my problem, but I mean this problem for device memory.
In other words I need to make scrollToRowAtIndexPath
Thanks!
You are right to identify the scrollToRowAtIndexPath method. So all you need to do is create a fresh IndexPath with the row you wish to move to, e.g., row index = 10:
[tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:10 inSection:indexPath.section]
atScrollPosition:UITableViewScrollPositionMiddle animated:NO];
//For iOS 7
[self.TableView reloadData];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:1 inSection:1];
[TableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:YES];
One can identify the row(in the eg. below it is forRow) that needs to be made visible and reload the row. Upon the completion of that one can scroll to that row.
let moveToIndexPath = NSIndexPath(forRow: forRow, inSection: 0)
self.tableView.reloadRowsAtIndexPaths([moveToIndexPath], withRowAnimation: .None)
self.tableView.scrollToRowAtIndexPath(moveToIndexPath, atScrollPosition: atScrollPosition, animated: true)
If you're trying to present a ÙITableViewController, calling tableView.reloadData() in viewDidLoad() or viewWillAppear() will cause noticeable UI lag before the presentation animation.
However, you will need to wait for the ÙITableView to have loaded its data and layed out its view, otherwise scrollToRow(at:, at:, animated:) will not work.
Here's the (slightly hacky) solution I came with up with for scrolling in a table view which is currently being presented:
private var needsToScroll = true
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if needsToScroll {
needsToScroll = false
DispatchQueue.main.async {
let indexPath = ...
tableView.scrollToRow(at: indexPath, at: .top, animated: false)
}
}
}
needsToScroll is needed because viewDidLayoutSubviews() will often be called more than once, which is an issue since scrollToRow(at:, at:, animated:) can result in viewDidLayoutSubviews() being called again.