I have a UICollectionView to display chat messages. At the beginning I load the existing messages to the collectionView and scroll the collectionView down to the last message with this method:
- (void)scrollToLastMessageAnimated:(BOOL)animated;
{
if (_messages.count == 0) { return; }
NSUInteger indexOfLastSection = _messagesBySections.count - 1;
NSInteger indexOfMessageInLastSection = [_messagesBySections[indexOfLastSection] count] - 1;
NSIndexPath *path = [NSIndexPath indexPathForItem:indexOfMessageInLastSection
inSection:indexOfLastSection];
[_collectionView scrollToItemAtIndexPath:path
atScrollPosition:UICollectionViewScrollPositionCenteredVertically
animated:animated];
}
This only works animated when I call it in the viewDidAppear: method and not in viewWillAppear: method. How can I scroll down without animation?
Here it is...
NSInteger section = [self.collectionView numberOfSections] - 1;
NSInteger item = [self.collectionView numberOfItemsInSection:section] - 1;
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:item inSection:section];
[self.collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:(UICollectionViewScrollPositionBottom) animated:YES];
I also provided my answer in another question of yours but I am also writing it here, since this answer is being related only with this problem
Solution
To scroll the view at the last index without crashing before view appears, you first need to trigger the reload of your collectionView data. After it has been reloaded call your method to scroll your view.
-(void)viewWillAppear:(BOOL)animated {
[collectionView reloadData];
[self scrollToLastMessageAnimated:YES];
}
Update
[collectionView setContentOffset:CGPointMake(0, CGFLOAT_MAX)]
Swift 3.0 Code :
let index = IndexPath(item: (self.mFetchedResultsControllerVar?.fetchedObjects?.count)! - 1, section: 0)
self.collectionView?.scrollToItem(at: index, at: UICollectionViewScrollPosition.bottom, animated: true)
A more foolproof solution that handles multiple/0 sections/items
extension UICollectionView {
func scrollToLastItem(at scrollPosition: UICollectionViewScrollPosition = .centeredHorizontally, animated: Bool = true) {
let lastSection = numberOfSections - 1
guard lastSection >= 0 else { return }
let lastItem = numberOfItems(inSection: lastSection) - 1
guard lastItem >= 0 else { return }
let lastItemIndexPath = IndexPath(item: lastItem, section: lastSection)
scrollToItem(at: lastItemIndexPath, at: scrollPosition, animated: animated)
}
}
This Extension scrolls to the last section that actually has items.
Best approach so far.
public extension UICollectionView {
func scrollToLastItem() {
scrollToLastItem(animated: true, atScrollPosition: .bottom)
}
func scrollToLastItem(animated: Bool, atScrollPosition scrollPosition: UICollectionViewScrollPosition) {
guard numberOfSections > 0 else {
return
}
var sectionWithItems: SectionInfo?
for section in Array(0...(numberOfSections - 1)) {
let itemCount = numberOfItems(inSection: section)
if itemCount > 0 {
sectionWithItems = SectionInfo(numberOfItems: itemCount, sectionIndex: section)
}
}
guard let lastSectionWithItems = sectionWithItems else {
return
}
let lastItemIndexPath = IndexPath(row: lastSectionWithItems.numberOfItems - 1, section: lastSectionWithItems.sectionIndex)
scrollToItem(at: lastItemIndexPath, at: scrollPosition, animated: animated)
}
}
The problem was actually caused by problematic Autolayout constraints,
see my other thread for the solution that helped me in the end: https://stackoverflow.com/a/24033650/2302437
Related
In order to achieve scroll To bottom for a table view, I am using the below code.
extension UITableView {
func scrollToBottom(){
let indexPath = IndexPath(
row: self.numberOfRows(inSection: self.numberOfSections -1) - 1,
section: self.numberOfSections - 1)
self.scrollToRow(at: indexPath, at: .bottom, animated: true)
}
}
This is working perfectly fine for all the devices whose versions are below 13, but in ios 13 it is not scrolling completely to last cell , it is stopping in between the last cell (approximate 40 pixel from bottom).
I also tried alternate ways by
setting content Offset
setting the scroll to visible rect all
having a delay for 1.0 seconds
but all of these having the same behaviour, not scrolling completely.
If you're facing this issue because of having different cells with different heights then below code will probably work for you:
private func moveTableViewToBottom(indexPath: IndexPath) {
tableView.scrollToRow(at: indexPath, at: .bottom, animated: false)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.tableView.scrollToRow(at: indexPath, at: .bottom, animated: false)
}
}
Try this
func scrollToBottom(){
DispatchQueue.main.async {
let indexPath = IndexPath(row: self.yourDataSourceArray-1, section: self.numberOfSections - 1)
self.tableView.scrollToRow(at: indexPath, at: .bottom, animated: true)
}
}
Thank Shivam Pokhriyal
It helps me work properly on iOS 13, But I don't know exactly why
Swift:
private func moveTableViewToBottom(indexPath: IndexPath) {
tableView.scrollToRow(at: indexPath, at: .bottom, animated: false)
if #available(iOS 13.0, *) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.tableView.scrollToRow(at: indexPath, at: .bottom, animated: false)
}
}
}
OC:
- (void)scrollToBottomAnimated:(BOOL)animated {
NSInteger rows = [self.tableView numberOfRowsInSection:0];
if (rows > 0) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:rows-1 inSection:0];
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:animated];
if (#available(iOS 13.0, *)) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSInteger rows = [self.tableView numberOfRowsInSection:0];
if (rows > 0) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:rows-1 inSection:0];
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:animated];
}
});
} else {
// Fallback on earlier versions
}
}
}
In my Swift app I have a UITableView and UITextView. The idea is simple, when user adds a text - it should appear at the bottom of the table view.
So I have an array of my object SingleMessage:
var messages = [SingleMessage]()
When user adds a text to UITextView, I send message with Socket.IO and receive it:
func messageArrived(_ notification: Notification) {
if let message = (notification as NSNotification).userInfo?["message"] as? SingleMessage {
DispatchQueue.main.async(execute: { () -> Void in
messages.append(message)
self.tview.reloadData()
self.scrollToBottom()
)}
}
}
my function scrollToBottom() contains the following code:
if(self.messages.count > 0) {
let iPath = IndexPath(row: self.tview.numberOfRows(inSection: 0)-1, section: self.tview.numberOfSections-1)
self.tview.scrollToRow(at: iPath, at: UITableViewScrollPosition.bottom, animated: false)
}
and then I have cellForRow function, that does a lot of stuff, like setting fonts and texts for each label, etc.
override func tableView(_ tview: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tview.dequeueReusableCell(withIdentifier: "chat") as! SingleCommentCell
if let msg:SingleMessage = self.messages[(indexPath as NSIndexPath).row] as? SingleMessage {
.
.
.
My problem is that when I type something, then presses the send button immediately and start typing again - the whole interface freezes for couple seconds and I don't even see the feedback from the keyboard. I think that the problem is that the table view has to be completely refreshed.
I'm using the interface above in the Chat component, so the problem occurs not only when user quickly types several messages in a row, but also when there're many incoming messages.
Is there any way of speeding up the whole interface, like for example add new cells at the bottom of the table view and avoid refreshing already existing ones?
The other functions related to my UITableViewController are:
override func tableView(_ tview: UITableView, numberOfRowsInSection section: Int) -> Int {
return messages.count
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
Then I have:
override func viewDidLoad() {
super.viewDidLoad()
tview.separatorStyle = UITableViewCellSeparatorStyle.none
tview.backgroundColor = UIColor.clear
tview.delegate = self
tview.dataSource = self
self.tview.estimatedRowHeight = 100
NotificationCenter.default.addObserver(self, selector: #selector(ChatView.messageArrived(_:)), name: NSNotification.Name(rawValue: incomingMessage), object: nil)
}
reloadData is a very expensive operation. It rebuilds the entire table. You are better off keeping better track of your model, using insert and delete row functions when you want to perform those operations and refresh individual rows when they change.
A good strategy for this is to keep the old model, generate the new model, then compute the set of items that were created, moved, or removed and generate individual table operations for each case. Here is a bit of sample code:
- (void) setDevicesForKey: (NSString *) propertyKey
toDevices: (NSArray *) newDevices
{
NSArray *currentDevices = [self valueForKey: propertyKey];
NSUInteger tableSection = [self sectionForKey: propertyKey];
NSIndexSet *indexesOfItemsToRemove = [currentDevices indexesOfObjectsPassingTest: ^BOOL(DeviceItem * itemToCheck, NSUInteger idx, BOOL *stop) {
return ![newDevices containsObject: itemToCheck];
}];
NSIndexSet *indexesOfItemsToAdd = [newDevices indexesOfObjectsPassingTest:^BOOL(DeviceItem *itemToCheck, NSUInteger idx, BOOL *stop) {
return ![currentDevices containsObject: deviceItem];
}];
UITableView *tableView = [self tableView];
[tableView beginUpdates];
{
NSMutableArray *removeIndexPaths = [NSMutableArray array];
[indexesOfItemsToRemove enumerateIndexesUsingBlock: ^(NSUInteger idx, BOOL *stop) {
[removeIndexPaths addObject: [NSIndexPath indexPathForItem: idx inSection: tableSection]];
}];
[tableView deleteRowsAtIndexPaths: removeIndexPaths withRowAnimation: UITableViewRowAnimationAutomatic];
NSMutableArray *insertIndexPaths = [NSMutableArray array];
[indexesOfItemsToAdd enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
[insertIndexPaths addObject: [NSIndexPath indexPathForItem: idx inSection: tableSection]];
}];
[tableView insertRowsAtIndexPaths: insertIndexPaths withRowAnimation: UITableViewRowAnimationAutomatic];
[newDevices enumerateObjectsUsingBlock: ^(DeviceItem *itemToCheck, NSUInteger idx, BOOL *stop) {
if([currentDevices containsObject: itemToCheck])
{
NSUInteger oldIndex = [currentDevices indexOfObject: ticketToCheck];
NSUInteger newIndex = [newDevices indexOfObject: ticketToCheck];
if(oldIndex != newIndex)
{
NSIndexPath *fromIndexPath = [NSIndexPath indexPathForRow: oldIndex inSection: tableSection];
NSIndexPath *toIndexPath = [NSIndexPath indexPathForRow: newIndex inSection: tableSection];
[tableView moveRowAtIndexPath: fromIndexPath toIndexPath: toIndexPath];
}
}
}];
[self setValue: newDevices forKey: propertyKey];
}
[tableView endUpdates];
}
I recommend to insert the row with insertRows(at rather than calling reloadData and scroll only if the cell is not visible.
func messageArrived(_ notification: Notification) {
if let message = notification.userInfo?["message"] as? SingleMessage {
DispatchQueue.main.async {
// if the index path is created before the item is inserted the last row is self.messages.count
let newIndexPath = IndexPath(row: self.messages.count, section: 0)
self.messages.append(message)
self.tableView.insertRows(at: [newIndexPath], with: .automatic)
if let visiblePaths = self.tableView.indexPathsForVisibleRows, !visiblePaths.contains(newIndexPath) {
self.tableView.scrollToRow(at: newIndexPath, at: .bottom, animated: false)
}
}
}
}
Note:
The restriction of 8 characters for variable names is all over for more than 30 years.
Names like tview are hard to read. I'm using tableView in the code.
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
}
}
I want to get the last indexpath of a uicollectionview.
I tried this:
NSInteger section = [self numberOfSectionsInCollectionView:self.collectionView] - 1;
NSInteger item = [self collectionView:self.collectionView numberOfItemsInSection:section] - 1;
NSIndexPath *lastIndexPath = [NSIndexPath indexPathForItem:item inSection:section];
But couldn't find the last indexpath of an uicollectionview .
Any suggestions,
Thanks in advance.
NSInteger lastSectionIndex = [tableView numberOfSections] - 1;
NSInteger lastRowIndex = [tableView numberOfRowsInSection:lastSectionIndex] - 1;
// Now just construct the index path
NSIndexPath *pathToLastRow = [NSIndexPath indexPathForRow:lastRowIndex inSection:lastSectionIndex];
Answer for Swift 3
extension UICollectionView {
func scrollToLastIndexPath(position:UICollectionViewScrollPosition, animated: Bool) {
self.layoutIfNeeded()
for sectionIndex in (0..<self.numberOfSections).reversed() {
if self.numberOfItems(inSection: sectionIndex) > 0 {
self.scrollToItem(at: IndexPath.init(item: self.numberOfItems(inSection: sectionIndex)-1, section: sectionIndex),
at: position,
animated: animated)
break
}
}
}
}
Note that my experience shows that animated property should be false for initializing the collectionView.
From the code above, as an answer to your question use this method:
func lastIndexPath() -> IndexPath? {
for sectionIndex in (0..<self.numberOfSections).reversed() {
if self.numberOfItems(inSection: sectionIndex) > 0 {
return IndexPath.init(item: self.numberOfItems(inSection: sectionIndex)-1, section: sectionIndex)
}
}
return nil
}
I'm trying to select all UICollectionViewCells after a UIButton is tapped.
How do I do this?
Updated for Swift 3
Just Shadow's Answer updated to Swift 3
for i in 0..<assetCollectionView.numberOfSections {
for j in 0..<assetCollectionView.numberOfItems(inSection: i) {
assetCollectionView.selectItem(atIndexPath: IndexPath(row: j, section: i), animated: false, scrollPosition: .none)
}
}
You can select all cells in the first section through:
for (NSInteger row = 0; row < [self.collectionView numberOfItemsInSection:0]; row++) {
[self.collectionView selectItemAtIndexPath:[NSIndexPath indexPathForRow:row inSection:0] animated:NO scrollPosition:UICollectionViewScrollPositionNone];
}
If you have more than 1 section, just use another nested for loop to loop through all the sections.
Updated to Swift 4
Updated Indrajit Sinh Rayjada's answer and put into an extension, as this is so general that it really should be an extension.
extension UICollectionView {
func selectAll() {
for section in 0..<self.numberOfSections {
for item in 0..<self.numberOfItems(inSection: section) {
self.selectItem(at: IndexPath(item: item, section: section), animated: false, scrollPosition: [])
}
}
}
}
Here's the solution:
for (NSInteger i = 0; i < [_assetCollectionView numberOfSections]; i++)
{
for (NSInteger j = 0; j < [_assetCollectionView numberOfItemsInSection:i]; j++)
{
[_assetCollectionView selectItemAtIndexPath:[NSIndexPath indexPathForRow:j inSection:i] animated:NO scrollPosition:UICollectionViewScrollPositionNone];
}
}