collectionview stops scrolling when calling performBatchUpdates on new items - swift

I'm inserting new items as the user scrolls down the collection view... I'm having an issue where if they scroll too fast the collection view stops scrolling when the new items are inserted.
My original issue was that I was just calling self.collectionView.reloadData() instead of performing batch updates but it's currently doing the same exact thing...
This is my code
let postIndex = self.posts.count
let newPostIndex = newPosts.count
let indexArray = (postIndex...postIndex+newPostIndex-1).map{IndexPath(item: $0, section: 0)}
self.posts.append(contentsOf: newPosts)
self.collectionView.performBatchUpdates({
self.collectionView.insertItems(at: indexArray)
}, completion: nil)
I already have posts so I create an array of the indexes that I need to update and insert them in the performBatchUpdates but it looks really choppy and lacks a smooth feel.
I tried CATransaction as suggested here https://stackoverflow.com/a/32691888/6513002 and it literally scrolled all the way down completely bypassing everything new and going to the end..
Please assist

Related

Update indexPath value after deleting a row

I have a list of favorite movies. In the table cell, there is a button to delete the movie from the favorites list. So I want to animate it with tableView.deleteRows. But if I delete a row from the top of the screen the other rows coming upward. It is normal but when I was deleting the row from the top of the screen other cells that already shown in the list are not updating indexPath. I assign an asyncAfter method for tableView.reloadData to solve the problem but I think that can cause a crash on the OS side. Because I forced to main thread reload data after a delay. Is it really a problem for OS and what should I do?
Example problem:
If I delete the movie which has the 0 index all of the movies coming upward. So before deletion, 1 index movie should has 0 index but it not. If I tried to delete a new 0 index movie it is deleting another movie.
Work example without tableView.reloadData()
Here is my code // This is working but I think asyncAfter is a problem
cell.deleteButtonActionBlock = {
FavouritesHandler.shared.deleteMovie(movie)
self.tableView.deleteRows(at: [indexPath], with: .automatic)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
self.tableView.reloadData()
}
}
You must not use the captured index path if cells can be deleted, inserted or moved.
In the custom cell declare the closure
var deleteButtonActionBlock : ((UITableViewCell) -> Void)?
and call it
deleteButtonActionBlock?(self)
In cellForRow get the actual index path for the cell
cell.deleteButtonActionBlock = { aCell in
let actualIndexPath = tableView.indexPath(for: aCell)!
FavouritesHandler.shared.deleteMovie(movie)
tableView.deleteRows(at: [actualIndexPath], with: .automatic)
}
No (ugly) delay and no (pointless) reloading needed.
Please note also that I removed the self in the delete line because the tableView instance is available as method parameter.

CollectionView: Unable to animate ReloadItems

ReloadItems does not seem to allow for animation.
I tried using collectionView:( willDisplay) but the reloadItems() actually overtakes the animation (i.e. cutting it off and reloading the cell)
I tried performBatchUpdates() but I get outOfRange error in this block:
collectionView.performBatchUpdates({
// reload one or more items
collectionView.reloadItems(at: [tappedArray[0], tappedArray[1]])
}) { (_) in
// do animations here
if let cell1 = collectionView.cellForItem(at: self.tappedArray[1]) {
self.animateCell(cell1)
}
}
If a single item in a collectionView is reloaded, how can I add animation(s) to the re-display of that item?
Issue resolution: Animation done in a closure is NOT on the main thread. Since all animation should be on the main thread, this was causing my UI to glitch, or "not allow for animation".

Prefer Large Titles and RefreshControl not working well

I am using this tutorial to implement a pull-to-refresh behavior with the RefreshControl. I am using a Navigation Bar. When using normal titles everything works good. But, when using "Prefer big titles" it doesn't work correctly as you can see in the following videos. Anyone knows why? The only change between videos is the storyboard check on "Prefer Large Titles".
I'm having the same problem, and none of the other answers worked for me.
I realised that changing the table view top constraint from the safe area to the superview fixed that strange spinning bug.
Also, make sure the constant value for this constraint is 0 🤯.
At the end what worked for me was:
In order to fix the RefreshControl progress bar disappearing bug with large titles:
self.extendedLayoutIncludesOpaqueBars = true
In order to fix the list offset after refreshcontrol.endRefreshing():
let top = self.tableView.adjustedContentInset.top
let y = self.refreshControl!.frame.maxY + top
self.tableView.setContentOffset(CGPoint(x: 0, y: -y), animated:true)
If you were using tableView.tableHeaderView = refreshControl or tableView.addSubView(refreshControl) you should try using tableView.refreshControl = refreshControl
It seems there are a lot of different causes that could make this happen, for me I had a TableView embedded within a ViewController. I set the top layout guide of the tableview to the superview with 0. After all of that still nothing until I wrapped my RefreshControl end editing in a delayed block:
DispatchQueue.main.async {
if self.refreshControl.isRefreshing {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: {
self.refreshControl.endRefreshing()
})
}
}
The only working solution for me is combining Bruno's suggestion with this line of code:
tableView.contentInsetAdjustmentBehavior = .always
I've faced the same problem. Call refreshControl endRefreshing before calling further API.
refreshControl.addTarget(controller, action: #selector(refreshData(_:)), for: .valueChanged)
#objc func refreshData(_ refreshControl: UIRefreshControl) {
refreshControl.endRefreshing()
self.model.loadAPICall {
self.tableView.reloadData()
}
}
The only solution that worked for me using XIBs was Bruno's one:
https://stackoverflow.com/a/54629641/2178888
However I did not want to use a XIB. I struggled a lot trying to make this work by code using AutoLayout.
I finally found a solution that works:
override func loadView() {
super.loadView()
let tableView = UITableView()
//configure tableView
self.view = tableView
}
I had this issue too, and i fixed it by embedded my scrollView (or tableView \ collectionView) inside stackView, and it's important that this stackView's top constraint will not be attached to the safeArea view (all the other constraints can). the top constraint should be connect to it's superview or to other view.
I was facing the same issue for very long, the only working solution for me was adding refresh control to the background view of tableview.
tableView.backgroundView = refreshControl
Short Answer
I fixed this by delaying calling to API until my collection view ends decelerating
Long Answer
I notice that the issue happens when refresh control ends refreshing while the collection view is still moving up to its original position. Therefore, I delay making API call until my collection view stops moving a.k.a ends decelerating. Here's a step by step:
Follow Bruno's suggestion
If you set your navigation bar's translucent value to false (navigationBar.isTranslucent = false), then you will have to set extendedLayoutIncludesOpaqueBars = true on your view controller. Otherwise, skip this.
Delay api call. Since I'm using RxSwift, here's how I do it.
collectionView.rx.didEndDecelerating
.map { [unowned self] _ in self.refreshControl.isRefreshing }
.filter { $0 == true }
.subscribe(onNext: { _ in
// make api call
})
.disposed(by: disposeBag)
After API completes, call to
refreshControl.endRefreshing()
Caveat
Do note that since we delay API call, it means that this whole pull-to-refresh process is not as quick as it could have been done without the delay.
Unfortunately, no advice helped. But I found a solution that helped me. Setting the transparency of the navigation bar helped.enter image description here
Problem can be solved if add tableview or scroll view as root view in UIViewController hierarchy (like in UITableViewController)
override func loadView() {
view = customView
}
where customView is UITableView or UICollectionView

tableViewSelectionDidChange with addSubview and removeFromSuperview in table row

I looked for posts for issues with all 3 functions listed in the title, but my problem is not discussed anywhere. Frankly, my problem is such that it defies logic IMHO.
On selection of a row in my view-based NSTableView, I want to show a "Save" button in my last (aka "Action") column. So on viewDidLoad() I created my orphan button. Then on tableViewSelectionDidChange() I remove it from the superview and add it to my column's NSTableCellView. When there's no row selected, there is no button to show. Simple. Below code works perfectly.
func tableViewSelectionDidChange(_ notification: Notification) {
let selectedRow = tableView.selectedRow
btnSave!.removeFromSuperview()
if selectedRow > -1,
let rowView = rowView(selectedRow),
let actionView = rowView.view(atColumn: Column.action.hashValue) as? NSTableCellView {
actionView.addSubview(btnSave!)
}
}
Wait. Did I say Perfect? It works as long as I use mouse to change the selected row, including selecting below the last row in table which removes selection and any button in previous selected row. However, when I use the keyboard Up/Down keys to change the selection, it does not work.
First I thought the function will get called only if I use mouse to change row selection. However, that's not true. That's true for tableViewSelectionIsChanging as per docs and as per facts. But for tableViewSelectionDidChange docs don't say it will only work when mouse is used and the facts bear that out. I put print statements and function does get called. I stepped through debugger as well. The mind boggling part is - the method to remove button from superview works, but the one to add button as subview does not work - and only if I use keyboard. How is it possible that the same exact code executes but I get two different outcomes?
Adding remaining functions
I use this to get selected row
private func rowView(_ rowIndex: Int, _ make: Bool = false) -> NSTableRowView? {
return tableView.rowView(atRow: rowIndex, makeIfNecessary: make)
}
I call this in viewDidLoad to create my orphan button
private func createButton() {
btnSave = NSButton(frame: NSRect(x: 10, y: 0, width: 22, height: 16))
btnSave?.title = "S"
btnSave?.setButtonType(.momentaryPushIn)
btnSave?.bezelStyle = .roundRect
}

Move UICollectionViewCell that is currently not loaded

I'm using Firebase to create a networking app for a client.
For every user, I add an observer to its Firebase property timestamp, that means that when the user uploads a status update, the Firebase observer gets triggered so that the users cell in the feed move to the top (every user only has one cell). Here's the Firebase code:
Database.database().reference().child(forUser.userID).child("timestamp").observe(.value) { (snapshot) in
if let timestamp = snapshot.value as? Int {
if let cell = self.feedCollectionView.cellForItem(at: indexPath) as? FeedCell {
self.feedCollectionView.moveItem(at: indexPath, to: IndexPath(row: 0, section: 0))
}
}
}
It is though very well possible that I have 100-200 users and maybe the last of them updates his status and has to be moved to the top, even though his collection view cell has never even been loaded.
When I run this, it prints for every visible cell that it is movable and for every invisible cell that it can't access the unloaded cell.
How can I access a cell that has never been loaded and move it to the top?
Instead of moving a cell that may or may not be loaded, you should update the collectionview's datasource by sorting it on timestamp. Then you can call self.feedCollectionView.reloadData() and it'll place it on the top automatically.