TableView collapsing cell while scrolling to another: weird behavior - ios

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.

Related

Auto Select Middle Visible Cell Of Collection View

I'm trying to select and highlight the middle cell of the visible cells in a collection view at any given time. The collection view in question displays days for six months forwards and back.
I've tried using the scroll view delegates and the collection view delegates. But all that works is select and highlight code in didSelectItem() collection view delegate.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print("delegate called")
collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
collectionView.cellForItem(at: indexPath)?.backgroundColor = UIColor.highlightCellGreen()
if let cell = collectionView.cellForItem(at: indexPath) as? ClientListDateCollectionViewCell{
monthLabel.text = cell.monthName
monthLabel.text = monthLabel.text?.capitalized
}
I tried to select the middle cell while scrolling using the viewDidScroll() delegate. But, I wasn't able to get the output I wanted.
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let visibleCellCount = dateCollectionView.indexPathsForVisibleItems.count
let cellCount = dateCollectionView.visibleCells.count
let visibleCells = dateCollectionView.indexPathsForVisibleItems[visibleCellCount-1/2]
if visibleCellCount>0{
let middle = visibleCellCount/2
let midValue = dateCollectionView.indexPathsForVisibleItems[middle]
dateCollectionView.selectItem(at: midValue, animated: true, scrollPosition: .centeredHorizontally)
}
How do I go about selecting the middle cell?
edit 1: The collection view starts on the leftmost point and then scrolls to the middle i.e, today's date
You can use delegate of UICollectionView (i.e: didHighlightItemAtIndexPath). just make sure to call collection view delegates on your desired time by calling reload function
self.collectionView.reloadData()
and in you collection view delegate just do this
func collectionView(collectionView: UICollectionView, didHighlightItemAtIndexPath indexPath: NSIndexPath){
var cell : UICollectionViewCell = UICollectionViewCell()
self.collectionView.cellForItemAtIndexPath = indexPath
//change highlighted color as of your need
cell.view.backgroundColor = UIColor.init(red: 25, green: 118, blue: 210).cgColor
}
This will highlight you selected item
Disable multiple selection (or selection entirely?) to make things easier.
collectionView.allowsMultipleSelection = false
On scrollViewDidScroll(_:) get the center point of the screen as CGpoint.
let center = collectionView.center
Use that information to get the index path of the center item
let indexPath = collectionView.indexPathForItem(at: center)
Select the item
collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .top)
Suppose that you have the horizontal of displaying, and you want to have the auto scroll to the center of your item in datasource.
Creating a method and calling it immediately after your collection view is completely configured:
func scrollToCenterIndex() {
let centerIndex = LIST_OF_YOUR_DATA_SOURCE.count / 2
let indexPath = IndexPath(item: centerIndex, section: 0)
self.collectionView.scrollToItem(at: indexPath,
at: .right,
animated: false)
}
Inside the method:
public func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CELL,
for: indexPath) as? CustomCell else {
fatalError("Cannot create cell")
}
If indexPath.row == LIST_OF_YOUR_DATA_SOURCE.count / 2 {
// perform your hight light color to the cell
} else {
// reset your hight light color to default color
}
let model = LIST_OF_YOUR_DATA_SOURCE[indexPath.row]
cell.configure(model)
return cell
}
I think you can use a method to get the center point of collection view, and use this value to get the the middle of visible cell.
let centerPoint = self.view.convert(collectionView.center, to: collection)
Here is an example I did it with a tableView. You can apply it to your collection view with the same approach.
class ViewController: UIViewController {
#IBOutlet weak var tableView: UITableView!
var dataSource = Array(1...31)
var centerIndex: IndexPath?
func setCellSelected(cell: UITableViewCell, _ selected: Bool) {
cell.contentView.backgroundColor = selected ? .green : .white
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
dataSource.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "CELL")
cell?.textLabel?.text = String(dataSource[indexPath.row])
let center = self.view.convert(tableView.center, to: tableView)
if let index = tableView.indexPathForRow(at: center), let cell = cell {
setCellSelected(cell: cell, indexPath.row == index.row)
}
return cell!
}
}
extension ViewController: UITableViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// reset the previous hight light cell
if let centerIndex = centerIndex, let cell = tableView.cellForRow(at: centerIndex) {
setCellSelected(cell: cell, false)
}
// set hight light to a new center cell
let center = self.view.convert(tableView.center, to: tableView)
if let index = tableView.indexPathForRow(at: center), let cell = tableView.cellForRow(at: index) {
setCellSelected(cell: cell, true)
centerIndex = index
}
}
}
I was also trying to do the auto-selection of the middle visible cell of the collection view, and I got the solution, here is the solution:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
// Reload Collection View
collectionView.reloadData()
// Find centre point of collection view
let visiblePoint = CGPoint(x: collectionView.center.x + collectionView.contentOffset.x, y: collectionView.center.y + collectionView.contentOffset.y)
// Find index path using centre point
guard let newIndexPath = collectionView.indexPathForItem(at: visiblePoint) else { return }
// Select the new centre item
collectionView.selectItem(at: newIndexPath, animated: true, scrollPosition: .centeredHorizontally) }
You need to use the Scroll view delegate function, scrollViewDidEndDecelerating. Reload the collection view first. Second, find the center visible point of the collection view. Third, using the center visible point, find the indexPath of collection view and finally use the index to select the item in the collection view.
I know I answered this question a little late, still thinking that it will be helpful for someone.
Cheers!

weird tableView animation when using insert function

I'm trying to insert number more cells in the table view when the user reaches the bottom of the table. the insert is working fine, but a weird animation is happening when scrolling up.
I tried to add beginUpdates() and endUpdates() before and after the insert() method. but this didn't help. I tried to add tableView.contentInsetAdjustmentBehavior = .never in viewDidLoad and it didn't help.
the code I tried is:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if tableView.contentOffset.y >= (tableView.contentSize.height - tableView.frame.size.height) {
loadMoreCells()
}
func loadMoreCells() {
ServerRequests.getDataServerRequest(id: provider.ID, page: page) { (foundEpisodes) in
if foundEpisodes.count > 0 {
let previous = self.episodes.count
self.episodes.insert(contentsOf: foundEpisodes, at: self.episodes.count)
self.isViewOpen.insert(contentsOf: [Bool](repeatElement(false, count: foundEpisodes.count)), at: self.isViewOpen.count)
var indexPathsToBeInserted = [IndexPath]()
for i in previous..<self.episodes.count{
indexPathsToBeInserted.append(IndexPath(row: i, section: 0))
}
self.tableView.insertRows(at: indexPathsToBeInserted, with: UITableViewRowAnimation.none)
}
}
}
Am I calling the insert in the wrong place? or what is the wrong thing I am doing ?
UPDATE
in viewDidLoad:
tableView.register(UINib(nibName: NIB_NAME, bundle: nil), forCellReuseIdentifier: NIB_IDENTIFIRE)
in cellForRow:
let cell = tableView.dequeueReusableCell(withIdentifier: NIB_IDENTIFIRE, for: indexPath) as! EpisodePerProviderTableViewCell
cell.setUpCell(tableView: tableView, episode: episodes[indexPath.row], indexPath: indexPath, isViewOpen: isViewOpen)
return cell
the setUpCell function is:
unc setUpCell(tableView: UITableView, episode: Episode, indexPath: IndexPath, isViewOpen: [Bool]) {
isMenuVisible = false
menuView.isHidden = true
if (isViewOpen[indexPath.row]){
descriptionLabel.numberOfLines = 100
moreLessLabel.text = "Less"
}else{
descriptionLabel.numberOfLines = 2
moreLessLabel.text = "More"
}
tableView.beginUpdates()
tableView.endUpdates()
self.isViewOpen = isViewOpen
self.indexPath = indexPath
self.tableView = tableView
self.episode = episode
arrowButton.transform = .identity
//set up cell content
if let myDate = dateFormatter.date(from: episode.episodePostDate) {
postedDate.text = dateFormatter.timeSince(from: myDate as NSDate)
}
episodeImage.windless.start()
episodeImage.sd_setImage(with: URL(string: episode.episodeImageUrl ?? ""), placeholderImage: UIImage(named: "FalloundPlayerLogo")) { (providerImage, error, SDImageCacheType, url) in
self.episodeImage.windless.end()
}
title.text = episode.episodeName
durationLabel.text = "".formatted(time: Float(episode.episodeDuration), withoutSec: true)
descriptionLabel.text = episode.episodeDescription
}
I think the problem is because loadMoreCells is called too many times.
try using this check instead of willDisplay:
if indexPath.row == yourArrayOfCells.count - 1{
loadMoreCells()
}
UPDATE
So I have some how of the same issue, after some digging I found this solution for myself :
//conform to UIScrollViewDelegate
let threshold : CGFloat = 10.0 // threshold from bottom of tableView
var isLoadingMore = false //checks if API session ended
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let contentOffset = scrollView.contentOffset.y
let maximumOffset = scrollView.contentSize.height - scrollView.frame.size.height;
if !isLoadingMore && (maximumOffset - contentOffset) <= threshold {
self.isLoadingMore = true
DispatchQueue.main.async {
loadMoreCells()
self.isLoadingMore = false
}
}
}
If the API request happening too fast and the load looks funny you can add a delay :
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
loadMoreCells()
self.isLoadingMore = false
}
If it won't work I will assume that the issue is not in the code you provided.
UPDATE-2
Those 2 funcs in setUpCell() aren't neccessery :
tableView.beginUpdates()
tableView.endUpdates()
Hope this helps.
UIView.setAnimationsEnabled(false)
self.tableView.beginUpdates()
self.tableView.insertRows(at: indexPathsToBeInserted, with: UITableViewRowAnimation.none)
self.tableView.endUpdates()
UIView.setAnimationsEnabled(true)

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

Detecting double and single tap on UITableViewCell to perform two action

I have a PFQueryTableViewController in which I am trying to learn some tableView interactions.
What I currently have: The tableView cell increases its height on a tap didSelectRowAtIndexPath or I can basically segue cell related data to next viewController using performSegueWithIdentifier together with prepareForSegue using a single tap.
In the below code, if I comment the "Tap code to increases height of tableView cell" code, I can detect double tap. Other wise the single tap increases the height as the code is inside didSelect block.
What I need:
On Single tap, cell height increases or decrease. While the cell height is increased, if I double tap, it should segue cell data to next UIViewController.
If not, the single tap should increase or decrease the height. And double tap should segue cell data to next UIViewController
Code is as below:
var selectedCellIndexPath: NSIndexPath?
var SelectedCellHeight = CGFloat() // currently set to 480.0 tableView height
var UnselectedCellHeight = CGFloat() // currently set to 300.0 tableView unselected hight
....
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if let selectedCellIndexPath = selectedCellIndexPath {
if selectedCellIndexPath == indexPath {
return SelectedCellHeight
}
}
return UnselectedCellHeight
}
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
var cell = tableView.dequeueReusableCellWithIdentifier("cell") as! DataViewTableViewCell!
if cell == nil {
cell = DataViewTableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "cell")
}
// Below is the double Tap gesture recognizer code in which
// The single tap is working, double tap is quite difficult to get
// as the cell height increases on single tap of the cell
let doubleTap = UITapGestureRecognizer()
doubleTap.numberOfTapsRequired = 2
doubleTap.numberOfTouchesRequired = 1
tableView.addGestureRecognizer(doubleTap)
let singleTap = UITapGestureRecognizer()
singleTap.numberOfTapsRequired = 1
singleTap.numberOfTouchesRequired = 1
singleTap.requireGestureRecognizerToFail(doubleTap)
tableView.addGestureRecognizer(singleTap)
// Tap code to increases height of tableView cell
if let selectedCellIndexPath = selectedCellIndexPath {
if selectedCellIndexPath == indexPath {
self.selectedCellIndexPath = nil
cell.postComment.fadeIn()
cell.postCreatorPicture.alpha = 1
cell.postDate.fadeIn()
// cell.postTime.fadeIn()
cell.postCreator.fadeIn()
} else {
self.selectedCellIndexPath = indexPath
}
} else {
selectedCellIndexPath = indexPath
}
tableView.beginUpdates()
tableView.endUpdates()
}
func doubleTap() {
print("Double Tap")
}
func singleTap() {
print("Single Tap")
}
The other way I can make it work is, if I can get NSIndexPath of selected cell outside of didSelectRowAtIndexPath code, I can basically use the double tap to perform if else to get the required action. Can you get NSIndexPath outside the tableView default blocks?
Got it working!
First: Define var customNSIndexPath = NSIndexPath() and add the below code inside the didSelectRowAtIndexPath code block.
customNSIndexPath = indexPath
print(customNSIndexPath)
Second: Remove "Tap code to increases height of tableView cell" code from didSelectRowAtIndexPath and add to the singleTap() function. And perform segue using the doubleTap().
func doubleTap() {
print("Double Tap")
performSegueWithIdentifier("MainToPost", sender: self)
}
func singleTap() {
print("Single Tap")
var cell = tableView.dequeueReusableCellWithIdentifier("cell") as! DataViewTableViewCell!
if cell == nil {
cell = DataViewTableViewCell(style: UITableViewCellStyle.Default, reuseIdentifier: "cell")
}
if let selectedCellIndexPath = selectedCellIndexPath {
if selectedCellIndexPath == customNSIndexPath {
self.selectedCellIndexPath = nil
} else {
self.selectedCellIndexPath = customNSIndexPath
}
} else {
selectedCellIndexPath = customNSIndexPath
}
tableView.beginUpdates()
tableView.endUpdates()
}
This is how you screw up working late without sleeping. It was the easiest thing to do. Thanks
Note: If someone has better solution or this one is wrong in some xyz case, please do comment. If yours is better it would really help others.
var lastClick: TimeInterval = 0.0
var lastIndexPath: IndexPath? = nil
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
let now: TimeInterval = Date().timeIntervalSince1970
if (now - lastClick < 0.3) && (lastIndexPath?.row == indexPath.row ) {
print("Double Tap!")
} else {
print("Single Tap!")
}
lastClick = now
lastIndexPath = indexPath
}

UITableView indexPath of last row

I am attempting to make the last row in a UITableView visible, after it has been added. Right now, when I add a row and call reloadData, the table goes to the top.
I figure if I get the indexPath for the last row, that I can select that row and it should appear in the list. I am unsure of how to get that value, or even if I am approaching this correctly.
How do I get an indexPath for a specific row?
Please note that, you don't need to call the reloadData to make the last row visible. You can make use of scrollToRowAtIndexPath method.
You can use the below code to achieve your goal.
// First figure out how many sections there are
let lastSectionIndex = self.tblTableView!.numberOfSections() - 1
// Then grab the number of rows in the last section
let lastRowIndex = self.tblTableView!.numberOfRowsInSection(lastSectionIndex) - 1
// Now just construct the index path
let pathToLastRow = NSIndexPath(forRow: lastRowIndex, inSection: lastSectionIndex)
// Make the last row visible
self.tblTableView?.scrollToRowAtIndexPath(pathToLastRow, atScrollPosition: UITableViewScrollPosition.None, animated: true)
Swift 4.0:
tableView.scrollToRow(at: indexPath, at: UITableViewScrollPosition.none, animated: true)
You can use scrollToRowAtIndexPath with extension:
In Swift 3:
extension UITableView {
func scrollToLastCell(animated : Bool) {
let lastSectionIndex = self.numberOfSections - 1 // last section
let lastRowIndex = self.numberOfRows(inSection: lastSectionIndex) - 1 // last row
self.scrollToRow(at: IndexPath(row: lastRowIndex, section: lastSectionIndex), at: .Bottom, animated: animated)
}
}
Shamsudheen TK's answer will crash
if there is no rows/sections in tableview.
The following solution to avoid crash at run time
extension UITableView {
func scrollToBottom() {
let lastSectionIndex = self.numberOfSections - 1
if lastSectionIndex < 0 { //if invalid section
return
}
let lastRowIndex = self.numberOfRows(inSection: lastSectionIndex) - 1
if lastRowIndex < 0 { //if invalid row
return
}
let pathToLastRow = IndexPath(row: lastRowIndex, section: lastSectionIndex)
self.scrollToRow(at: pathToLastRow, at: .bottom, animated: true)
}
}
Note: If you are trying to scroll to bottom in block/clousure then you need to call this on main thread.
DispatchQueue.main.async {
self.tableView.scrollToBottom()
}
Hope this will helps other
As suggested by others get indexPath for perticular sections like section 0.
After that call...add this methos in cellFOrROwAtIndex
[tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionTop animated:YES]; ..to scroll to specific indexPath in TableView.
Note:-But it still need scrolling of tableview in Downward direction.
You shouldn't be using -reloadData for this use case. What you're looking for is -insertRowsAtIndexPaths:withRowAnimation:.
Feel free to ask if you want some usage examples or a more detailed explanation as to why using -reloadData send you to the top of the UITableView.
Not mandatorily required to get the indexpath of last row.
You can set the CGPoint of UITableview to show a last row you added.
I always use this code in my chat application to show a last added message.
//Declaration
#IBOutlet weak var tableview: UITableView!
//Add this executable code after you add this message.
var tblframe: CGRect = tableview.frame
tblframe.size.height = self.view.frame.origin.y
tableview.frame = tblframe
var bottomoffset: CGPoint = CGPointMake(0, tableview.contentSize.height - tableview.bounds.size.height)
if bottomoffset.y > 0 {
tableview.contentOffset = bottomoffset;
}
I hope it will work for you.
Thanks.
Swift 5.0 +
extension UITableView {
func isLastVisibleCell(at indexPath: IndexPath) -> Bool {
guard let lastIndexPath = indexPathsForVisibleRows?.last else {
return false
}
return lastIndexPath == indexPath
}
}

Resources