CollectionView Programatic Scrolling - ios

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)

Related

Removing a cell from a UICollectionView has a weird animation

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!

TableView collapsing cell while scrolling to another: weird behavior

I have a tableView with chat bubbles.
Those bubbles are shortened if the character count is more than 250
If a user clicks on a bubble
The previous selection gets deselected (shortened)
The new selection expands and reveals the whole content
The new selections top constraint changes (from 0 to 4)
What I would like to achieve?
If a long bubble is selected already, but the user selects another bubble, I want the tableView to scroll to the position of the new selected bubble.
I'll share a video about it
Without this scrolling, the contentOffset remains the same and it looks bad.
(In the video: on the right)
Video:
Right: without the mentioned scrolling
Left: with scrolling
https://youtu.be/_-peZycZEAE
Here comes the problem:
On the left, you can notice that it is glitchy.
Random ghost cells are appearing for no reason.
Sometimes it even messes up the height of some bubbles (not in the video)
Why is it so?
Code:
func bubbleTappedHandler(sender: UITapGestureRecognizer) {
let touchPoint = sender.location(in: self.tableView)
if let indexPath = tableView.indexPathForRow(at: touchPoint) {
if indexPath == currentSelectedIndexPath {
// Selected bubble is tapped, deselect it
self.selectDeselectBubbles(deselect: indexPath)
} else {
if (currentSelectedIndexPath != nil){
// Deselect old bubble, select new one
self.selectDeselectBubbles(select: indexPath, deselect: currentSelectedIndexPath)
} else {
// Select bubble
self.selectDeselectBubbles(select: indexPath)
}
}
}
}
func selectDeselectBubbles(select: IndexPath? = nil, deselect: IndexPath? = nil){
var deselectCell : WorldMessageCell?
var selectCell : WorldMessageCell?
if let deselect = deselect {
deselectCell = tableView.cellForRow(at: deselect) as? WorldMessageCell
}
if let select = select {
selectCell = tableView.cellForRow(at: select) as? WorldMessageCell
}
// Deselect Main
if let deselect = deselect, let deselectCell = deselectCell {
tableView.deselectRow(at: deselect, animated: false)
currentSelectedIndexPath = nil
// Update text
deselectCell.messageLabel.text = self.dataSource[deselect.row].message.shortened()
}
// Select Main
if let select = select, let selectCell = selectCell {
tableView.selectRow(at: select, animated: true, scrollPosition: .none)
currentSelectedIndexPath = select
// Update text
deselectCell.messageLabel.text = self.dataSource[select.row].message.full()
}
UIView.animate(withDuration: appSettings.defaultAnimationSpeed) {
// Deselect Constraint changes
if let deselect = deselect, let deselectCell = deselectCell {
// Constarint change
deselectCell.nickNameButtonTopConstraint.constant = 0
deselectCell.timeLabel.alpha = 0.0
deselectCell.layoutIfNeeded()
}
// Select Constraint changes
if let select = select, let selectCell = selectCell {
// Constarint change
selectCell.nickNameButtonTopConstraint.constant = 4
selectCell.timeLabel.alpha = 1.0
selectCell.layoutIfNeeded()
}
}
self.tableView.beginUpdates()
self.tableView.endUpdates()
UIView.animate(withDuration: appSettings.defaultAnimationSpeed) {
if let select = select, deselect != nil, self.tableView.cellForRow(at: deselect!) == nil && deselectCell != nil {
// If deselected row is not anymore on screen
// but was before the collapsing,
// then scroll to new selected row
self.tableView.scrollToRow(at: select, at: .top, animated: false)
}
}
}
Update 1: Added Github project
Link: https://github.com/krptia/test2
I made a small version of my app, so that you can see and test what my problem is. I would be so thankful if someone could help to solve this issue! :c
First let's define what we mean by "without scrolling" - we mean that the cells more of less stay the same. So we want to find a cell that we want to be the anchor cell. From before the changes to after the changes the distances from the cell's top to the top of the screen is the same.
var indexPathAnchorPoint: IndexPath?
var offsetAnchorPoint: CGFloat?
func findHighestCellThatStartsInFrame() -> UITableViewCell? {
return self.tableView.visibleCells.filter {
$0.frame.origin.y >= self.tableView.contentOffset.y
}.sorted {
$0.frame.origin.y > $1.frame.origin.y
}.first
}
func setAnchorPoint() {
self.indexPathAnchorPoint = nil;
self.offsetAnchorPoint = nil;
if let cell = self.findHighestCellThatStartsInFrame() {
self.offsetAnchorPoint = cell.frame.origin.y - self.tableView.contentOffset.y
self.indexPathAnchorPoint = self.tableView.indexPath(for: cell)
}
}
we call this before we start doing stuff.
func bubbleTappedHandler(sender: UITapGestureRecognizer) {
self.setAnchorPoint()
....
Next we need to set the content offset after we are doing doing our changes so that the cell moves back to where it is suppose to.
func scrollToAnchorPoint() {
if let indexPath = indexPathAnchorPoint, let offset = offsetAnchorPoint {
let rect = self.tableView.rectForRow(at: indexPath)
let contentOffset = rect.origin.y - offset
self.tableView.setContentOffset(CGPoint.init(x: 0, y: contentOffset), animated: false)
}
}
Next we call it after we are done doing our changes.
self.tableView.beginUpdates()
self.tableView.endUpdates()
self.tableView.layoutSubviews()
self.scrollToAnchorPoint()
The animation can be a little strange because there is a lot going on at the same time. We are changing the content offset and the sizes of the cell at the same time, but if you place your finger next to the first cell that top is visible you will see it ends up in the correct place.
Try using this willDisplay api on UITableViewDelegate
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if (indexPath == currentSelectedIndexPath) {
// logic to expand your cell
// you don't need to animate it since still not visible
}
}
Replace your code for bubbleTappedHandler with this and run and check:
func bubbleTappedHandler(sender: UITapGestureRecognizer) {
let touchPoint = sender.location(in: self.tableView)
if let indexPath = tableView.indexPathForRow(at: touchPoint) {
if indexPath == currentSelectedIndexPath {
currentSelectedIndexPath = nil
tableView.reloadRows(at: [indexPath], with: .automatic)
} else {
if (currentSelectedIndexPath != nil){
if let prevSelectedIndexPath = currentSelectedIndexPath {
currentSelectedIndexPath = indexPath
tableView.reloadRows(at: [prevSelectedIndexPath, indexPath], with: .automatic)
}
} else {
currentSelectedIndexPath = indexPath
tableView.reloadRows(at: [indexPath], with: .automatic)
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: { [weak self] in
let currentIndexPath = self?.currentSelectedIndexPath ?? indexPath
self?.tableView.scrollToRow(at: currentIndexPath, at: .top, animated: false)
})
}
}
You can use tableView?.scrollToRow. For example
let newIndexPath = IndexPath(row: 0, section: 0) // put your selected indexpath and section here
yourTableViewName?.scrollToRow(at: newIndexPath, at: UITableViewScrollPosition.top, animated: true) // give the desired scroll position to tableView
Available scroll positions are .none, .top, .middle, .bottom
This glitch is because of a small mistake. Just do these 2 things and the issue will be resolved.
Set tableView's rowHeight and estimatedRowHeight in `viewDidLoad
tableView.estimatedRowHeight = 316 // Value taken from your project. This may be something else
tableView.rowHeight = UITableView.automaticDimension
Remove the estimatedHeightForRowAt and heightForRowAt delegate methods from your code.

Using an action to scroll CollectionView to the next cell

I have a horizontal collectionview that adds a new cell every time I tap the button. I am attempting to scroll the collectionview to the next cell once the cells are no longer visible each time I tap the button.
The code I have now is not working properly
Code:
#IBAction func random(_ sender: Any) {
resultsCollection.reloadData()
let collectionBounds = resultsCollection.bounds
let contentOffset = CGFloat(floor(resultsCollection.contentOffset.x - collectionBounds.size.width))
self.moveToFrame(contentOffset: contentOffset)
}
func moveToFrame(contentOffset : CGFloat) {
let frame: CGRect = CGRect(x : contentOffset ,y : resultsCollection.contentOffset.y ,width : resultsCollection.frame.width,height : resultsCollection.frame.height)
resultsCollection.scrollRectToVisible(frame, animated: true)
}
How can I fix this so that it scrolls correctly when I tap the button?
After reload your data, call this lines.
let lastItem = resultsCollection(resultsCollection, numberOfItemsInSection: 0) - 1
let indexPath = IndexPath(row: lastItem, section: 0)
resultsCollection.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
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)
}
}
}
Set IndexPath row as per your needs

How to focus last cell in the UITableview in ios

I am creating a chat interface.
User's message is put in a new UITable view cell.
And when update the table view, I use the following code.
extension UITableView {
func scrollToBottom() {
let rows = self.numberOfRows(inSection: 0)
if rows > 0 {
let indexPath = IndexPath(row: rows - 1, section: 0)
self.scrollToRow(at: indexPath, at: .bottom, animated: true)
}
}
}
Actually this works a little better, but there is something strange.
When I turn off the app and turn it on again, or after exiting the screen and entering again, the following issues arise.
The issue is that when I add a new cell, it goes up to the first cell in the table view and back down to the last cell.
See the issue(https://vimeo.com/266821436)
As the number of cells increases, the scrolling becomes too fast and too messy.
I just want to keep updating the last cell that is newly registered.
What should I do?
Please use DispatchQueue to Scroll because of the method you are fire is executed with tableView load data so we need to give time to scroll.
extension UITableView {
func scrollToBottom() {
let rows = self.numberOfRows(inSection: 0)
if rows > 0 {
DispatchQueue.main.async {
let indexPath = IndexPath(row: rows - 1, section: 0)
self.scrollToRow(at: indexPath, at: .bottom, animated: true)
}
}
}
}
or
func scrollToBottom(){
DispatchQueue.main.async {
let indexPath = IndexPath(row: self.array.count-1, section: 0)
self.tableView.scrollToRow(at: indexPath, at: .bottom, animated: true)
}
}
self.scrollToRow(at: indexPath.last ...)

tableView does not scroll to bottom when first presented

Upon first presenting my tableView controller I load data into my model array and then scroll the tableView to the bottom. However the tableView does not scroll all the way down. Just scrolls to an intermediate position. Subsequent scrolling using the same method always correctly scrolls to the bottom.
I have the following code in my viewDidLoad of the tableViewController:
override func viewDidLoad() {
super.viewDidLoad()
let fetchRequest: NSFetchRequest<Item> = NSFetchRequest(entityName: "Item")
do {
let fetchedResults = try context.fetch(fetchRequest)
if fetchedResults.count > 0 {
self.dataArray = fetchedResults
let indexPath = IndexPath(item: (dataArray.count - 1), section: 0)
self.tableView.scrollToRow(at: indexPath, at: UITableViewScrollPosition.bottom, animated: false)
}
} catch {
print("Could not fetch \(error)")
}
}
I am using an inputAccessoryView in my tableViewController. Additionally I am using automaticDimensions although I did try calculating each row height and it didn't make any difference.
Wondering why the tableView is scrolling to an arbitrary intermediate position and not the bottom. On all subsequent scrollToRowAt calls, the tableView correctly scrolls to the bottom.

Resources