I have a weird animation/behaviour when removing an item from a collection view. It happens only when I have two items and that I remove the second item first. Here are two gif.
The first gif is the expected animation, it happens when I delete the first item.
The second gif is the wrong behaviour, it looks like the entire collection view is being reloaded:
Here is the method called to remove the item when clicking on the button:
#objc func removeCell(_ notification: NSNotification) {
collectionView.performBatchUpdates({ () -> Void in
let indexPath = IndexPath(row: itemIndex, section: 0)
let indexPaths = [indexPath]
viewModels.remove(at: itemIndex)
collectionView.deleteItems(at: indexPaths)
pageControl.numberOfPages = min(10, viewModels.count)
tableViewDelegate?.performTableViewBatchUpdates(nil, completion: nil)
})
}
Thank you for your help!
Related
i am trying to use scrollToRow but when call this method tableview does not show any data sometimes. When i check UI inspector i can see table cells but does not seen on screen.
I tried to DispactQueue didn't solve this problem
var forIndex = 0
for item in filteredData {
if let firstCharacter = item.Name?.prefix(1), firstCharacter == char {
let indexpath = IndexPath(row: forIndex, section: 0)
self.tableView.reloadRows(at: [indexpath], with: UITableView.RowAnimation.top)
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500), execute: { [weak self] in
self?.tableView.scrollToRow(at: indexpath, at: UITableView.ScrollPosition.top, animated: true)
})
break
}
forIndex += 1
}
char is element of list like a,b,c,d... FilteredData is list of tableview elements
When debugging if scrollToRow method marked breakpoint it's working
Probably the reloadRows (at:) animation is still in progress when you kick of the next causing this strange behavior.
With the breakpoint, the reloadRows (at:) has a chance to finish and on continuing the scrollToRow kicks off after.
Try one of the following if this helps your situation assuming there is no issue with your data source:
1. Change
tableView.reloadRows(at: [indexpath], with: .top)
to
tableView.reloadRows(at: [indexpath], with: .none) so that the UITableView applies a default animation
OR
2. Increase your delay before kicking off the next animation
OR
3. Perform the reload without animation
UIView.performWithoutAnimation { [weak self] in
self?.tableView.reloadRows(at: [indexpath], with: .none)
}
tableView.scrollToRow(at: indexpath,
at: UITableView.ScrollPosition.top,
animated: true)
OR
4. Use beginUpdates and endUpdates which can be useful when performing batch operations on table views
tableView.beginUpdates()
tableView.reloadRows(at: [indexpath], with: .top)
tableView.scrollToRow(at: indexpath,
at: UITableView.ScrollPosition.top,
animated: true)
tableView.endUpdates()
Do any of these give you better results ?
Update
Looking more at the docs for reloadRows(at indexPaths:, here are some lines that stood out for me
Call this method if you want to alert the user that the value of a
cell is changing. If, however, notifying the user is not
important—that is, you just want to change the value that a cell is
displaying—you can get the cell for a particular row and set its new
value.
So it seems that in some situations animation might not be needed as the cell could be off screen or not needed, so the simplest way to change data is get the cell at the index path and change the data without this function.
Since you are running this in a loop, you most likely start the next indexPath's reloadRows before the previous indexPath's scrollToRow is complete and this can cause some unusual behavior.
Since UITableView does not have it's own completion handler, we can try using CoreAnimation with recursion which can be option 5.
Here is small example I prepared. My sample is like this:
I have 15 rows that are grey in color
I decide that I want to change the color of 4 rows
I store the index of 4 rows in a queue
I will change the color using tableView.reloadRows(at:
After changing the color, I want to scroll to that row using tableView.scrollToRow(at:
Here is how I accomplish that
First I extend the UITableView to use CoreAnimation block to do reloadRows and scrollToRow
extension UITableView
{
func reloadRows(at indexPaths: [IndexPath],
with animation: UITableView.RowAnimation,
completion: (() -> Void)?)
{
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
reloadRows(at: indexPaths, with: animation)
CATransaction.commit()
}
func scrollToRow(at indexPath: IndexPath,
at scrollPosition: UITableView.ScrollPosition,
animated: Bool,
completion: (() -> Void)?)
{
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
CATransaction.setAnimationDuration(2) // set what you want
scrollToRow(at: indexPath, at: scrollPosition, animated: animated)
CATransaction.commit()
}
}
Here is how I use this extension with my view controller set up
class TableViewAnimation: UITableViewController
{
let numberOfRows = 15
// Change the color of these rows in tableview
var colorChangeArray: [Int] = []
// Copy of colorChangeArray used in recursion
var colorChangeQueue: [Int] = []
// Change the color of row to this
private let colorToChange = UIColor.systemBlue
// Normal cell color
private let colorNormal = UIColor.gray
override func viewDidLoad()
{
super.viewDidLoad()
view.backgroundColor = .white
setUpNavigationBar()
setUpTableView()
}
private func setUpNavigationBar()
{
title = "Table View Animate"
let barButton = UIBarButtonItem(title: "Animate",
style: .plain,
target: self,
action: #selector(didTapAnimateButton))
navigationItem.rightBarButtonItem = barButton
}
private func setUpTableView()
{
tableView.register(CustomCell.self,
forCellReuseIdentifier: CustomCell.identifier)
}
#objc
func didTapAnimateButton()
{
// Queue all the rows that should change
// We will dequeue these in the animation function
// and the recursive function stops when the queue
// is empty
colorChangeQueue = [1, 3, 6, 12]
resumeAnimation()
}
// Recursion function rather than loops using our
// custom functions from extension
private func resumeAnimation()
{
if !colorChangeQueue.isEmpty
{
let rowToChange = colorChangeQueue.removeFirst()
print("starting \(rowToChange) animation")
let indexPath = IndexPath(row: rowToChange,
section: 0)
colorChangeArray.append(rowToChange)
tableView.reloadRows(at: [indexPath],
with: .top) { [weak self] in
self?.tableView.scrollToRow(at: indexPath,
at: .top,
animated: true,
completion: {
// recursively call the function again with a small delay
DispatchQueue.main.asyncAfter(deadline: .now() + 1)
{
print("complete \(rowToChange) animation")
self?.resumeAnimation()
}
})
}
}
}
}
Finally, here is the data source and delegate but nothing unique is happening here, just adding it for completeness
extension TableViewAnimation
{
override func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int
{
return numberOfRows
}
override func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell
= tableView.dequeueReusableCell(withIdentifier: CustomCell.identifier) as! CustomCell
if colorChangeArray.contains(indexPath.row)
{
cell.mainView.backgroundColor = colorToChange
}
else
{
cell.mainView.backgroundColor = colorNormal
}
cell.textLabel?.textAlignment = .center
cell.textLabel?.text = "Row \(indexPath.row)"
return cell
}
override func tableView(_ tableView: UITableView,
heightForRowAt indexPath: IndexPath) -> CGFloat
{
return 150
}
}
Now it seems like the animations of each of the row is sequenced properly:
And also if you see the print in the console, it is sequenced which did not happen with the loop method:
starting 1 animation
complete 1 animation
starting 3 animation
complete 3 animation
starting 6 animation
complete 6 animation
starting 12 animation
complete 12 animation
I'm working on the collection view list cell and I was wondering how to update the indexPath when I use the diffable data source and snapshot on iOS14.
In my ViewController's viewdidload, I called configureDataSource method which registers cell information. Also, I have a displayEvents method that fetches Events data from the backend and displays them on the collection view cell. The eventDataSource holds an array of Events, so when the Event data are fetched, it updates the array so the ViewController can display data.
I also have an addEvent function which is called from EventActionModalViewController, like a user types the event name and sends .save API request to store the data in the database. It works fine, I mean successfully add data backend and display the new event on the collection view list. However, the problem is the collection view's indexPath is not updated.
For instance, if there are already two events in the collection view, their indexPath are [0,0] and [0,1]. (I printed them out in print("here is the indexPath: \(indexPath)"))
And after I add the new event, there are three events on the collection view, which is correct, but the indexPath becomes [0,0],[0,0] and [0,1]. So I think the indexPath of already displayed events are not updated. Is applying a snapshot not enough to update the indexPath of each cell? I was thinking even after applying snapshot, it still needs to reload the collection view to apply a new indexPath to the cell, or something similar.
Has anyone faced the same issue? If so, how did you apply the new indexPath to the cell?
I also have a function to delete the cell, but it doesn't update the indexPath as well.
Btw, I'm working on the app for iOS14, so cannot use one for iOS15...
private var eventDataSource = EventDataSource()
override func viewDidLoad() {
super.viewDidLoad()
setupCollectionView() // -> create collection view and set constraints
configureDataSource()
displayEvents()
}
func configureDataSource() {
let cellRegistration = UICollectionView.CellRegistration<ListCell, Event> { cell, indexPath, Event in
cell.Event = Event
let moreAction = UIAction(image: Images.setting) { _ in
let vc = EventActionModalViewController();
vc.modalPresentationStyle = .overCurrentContext
print("here is the indexPath: \(indexPath)")
vc.indexPath = indexPath
self.tabBarController?.present(vc, animated: false, completion: nil)
}
let moreActionButton = UIButton(primaryAction: moreAction)
moreActionButton.tintColor = UIColor.ouchienLightGray()
let moreActionAccessory = UICellAccessory.CustomViewConfiguration(
customView: moreActionButton,
placement: .trailing(displayed: .whenEditing, at: { _ in return 0 })
)
cell.accessories = [
.customView(configuration: moreActionAccessory),
.disclosureIndicator(displayed: .whenNotEditing, options: .init(tintColor: .systemGray))
]
}
dataSource = UICollectionViewDiffableDataSource<Section, Event>(collectionView: collectionView) {
(collectionView, indexPath, Event) -> UICollectionViewCell? in
let cell = collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: Event)
return cell
}
}
func displayEvents() {
eventDataSource.fetchEvents { [weak self] in
self?.applySnapshot(animatingDifferences: true)
}
}
func addEvent(EventTitle: String) {
eventDataSource.save(eventTitle: EventTitle, completion: { [weak self] in
self?.applySnapshot(animatingDifferences: true)
})
}
func applySnapshot(animatingDifferences: Bool = true) {
snapshot = NSDiffableDataSourceSnapshot<Section, Event>()
snapshot.appendSections([.List])
snapshot.appendItems(eventDataSource.Events)
dataSource?.apply(snapshot, animatingDifferences: animatingDifferences)
}
The index path you pass in moreAction is the one captured at cell registration, which happens only if cell is created, but not recycled.
You need to call UICollectionView.indexPathForCell to get the right index at any time.
I use SwipeCellKit for do swipe actions for my tableview.
I try to do the left swipe for check or unckech for accessoryType of my cell and everything work fine, but after i press check the tableview reload the data immediatelly and i can't see the animation of roollback the check button. So i want to ask how i can call reload data after this animation end.
I have something like this:
I have something like this:
But i want fade animation like this
I want this animation
My code:
override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> [SwipeAction]? {
var action = super.tableView(tableView, editActionsForRowAt: indexPath, for: orientation) ?? []
guard orientation == .left else { return action }
let checkAction = SwipeAction(style: .destructive, title: nil) { action, indexPath in
self.completeItem(at: indexPath)
}
action.append(checkAction)
return action
}
private func completeItem(at indexPath: IndexPath){
if let item = itemArray?[indexPath.row]{
do{
try realm.write {
item.done = !item.done
}
}
catch{
print( error.localizedDescription)
}
}
tableView.reloadData()
}
In order to hide the action on select, you need to set the hidesWhenSelected property for the action.
checkAction.hidesWhenSelected = true
Also, the example doesn't reload the tableView. Instead, they use an animation to remove the dot. You will need to manually delay your reload until the hide animation is complete.
See line 156 for the property
See lines 256 - 273 for the animation
https://github.com/SwipeCellKit/SwipeCellKit/blob/develop/Example/MailExample/MailTableViewController.swift
What i am trying to do is scrolling my CollectionView to the very bottom as soon as my content has been loaded.
Here is my code;
func bindSocket(){
APISocket.shared.socket.on("send conversation") { (data, ack) in
let dataFromString = String(describing: data[0]).data(using: .utf8)
do {
let modeledMessage = try JSONDecoder().decode([Message].self, from: dataFromString!)
self.messages = modeledMessage
DispatchQueue.main.async {
self.collectionView.reloadData()
let indexPath = IndexPath(item: self.messages.count - 1, section: 0)
self.collectionView.scrollToItem(at: indexPath, at: .bottom, animated: false)
}
} catch {
//show status bar notification when socket error occurs
}
}
}
but it is totally not working.
By the way i'm using InputAccessoryView for a sticky data input bar like in iMessage and using collectionView.keyboardDismissMode = .interactive
thanks,
What I am think about that you maybe call the func before your collectionView getting visible cells or call reload collection view and then scroll items immediately, at this point scroll to indexPath will not make sense, since the collection view's content size is not yet updated.
scrollToItem(at:at:animated:)
Scrolls the collection view contents until the specified item is visible.
try this:
self.collectionView.reloadData()
let indexPath = IndexPath(item: self.messages.count - 1, section: 0)
let itemAttributes = self.collectionView.layoutAttributesForItem(at: indexPath)
self.collectionView.scrollRectToVisible(itemAttributes!.frame, animated: true)
If I side swipe on a table view row to delete it then the row does not dismiss as a complete entity - by this I mean the edit action portion of the cell always dismisses itself by sliding upwards, but the rest of the cell dismisses itself according to whatever value has been set for UITableViewRowAnimation.
So for example if the row below is deleted using deleteRows(... .left) then the white portion of the row will slide off to the left of the screen but the UNBLOCK part behaves separately - it always slides up the screen, irrespective of the UITableViewRowAnimation value used in deleteRows.
Code is as follows:
override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
let deleteAction = UITableViewRowAction(style: .default, title: "UNBLOCK") { action, index in
self.lastSelectedRow = indexPath
let callerToUnblock = self.blockedList[indexPath.row]
self.unblockCaller(caller: callerToUnblock)
}
deleteAction.backgroundColor = UIColor.tableViewRowDeleteColor()
return [deleteAction]
}
func unblockCaller(caller:Caller) {
DispatchQueue.global(qos: .background).async {
Model.singleton().unblockCaller(caller) {(error, returnedCaller) -> Void in
if error == nil {
DispatchQueue.main.async {
if let lastSelectedRow = self.lastSelectedRow {
self.tableView.deleteRows(at: [lastSelectedRow], with: .left)
}
}
...
Does anybody know how to make the whole row move consistently together?
Correct. These two objects behave as separate entities. You cannot control the animation of the UITableViewRowAction button as it drawn outside of the UITableViewCell frame.
See the UITableViewCellDeleteConfirmationView layout in this Captured View Hierarchy. (Notice how the UITableViewCell frame, in blue, doesn't encompass the UITableViewCellDeleteConfirmationView, in red):
Fix
Hide the editing button prior deleting the cell. It will be erased prior the cell deletion animation, before the rest of the cells scroll upward.
func unblockCaller(caller:Caller, indexPath:IndexPath) {
// Perform the deletion
// For example: self.blockedList.remove(at: indexPath.row)
// if error == nil...
DispatchQueue.main.async {
self.tableView.setEditing(false, animated: false)
self.tableView.deleteRows(at: [indexPath], with: .left)
}
}
Additional Comments
I recommend not using state variables to keep track of which cell must be deleted (self.lastSelectedRow = indexPath). Pass the path to the worker method instead for a better isolation of the tasks.
Use the standard appearance whenever possible ; if the UNBLOCK button is but a red control, prefer the style: .destructive and avoid custom color schemes. The editActionsForRowAt becomes a single line:
self.unblockCaller(caller: self.blockedList[indexPath.row], indexPath: indexPath)