Part of an app I'm working on involves a messaging interface which I'm trying to make similar to the iOS Messages app's interface. When new messages are received or sent, I'd like the table view to scroll down and display the insertion animation.
So far, I've essentially done this:
- (void)didReceiveNewMessage:(...) {
[messages addObject:...]; // add the new message to an array
NSIndexPath *newIndexPath = [NSIndexPath ...]; // find out where the new cell goes
[tableView insertRowAtIndexPath:newIndexPath ...]; // animate the insertion of the new message
[tableView scrollToRowAtIndexPath:newIndexPath ...]; // do the scrolling
}
This works if messages are coming in slowly. However, if I send myself a bunch of messages really quickly, I've noticed the table view will "become confused."
For example, if I quickly send the messages 1, 2, 3, 4, 5, 6, the table view will animate the insertion of 1, 2, 3, and 4, and then just stop scrolling. If I actually scroll the table view down manually, I can see 5 and 6 below. Anyone know of a better way to do this?
I did try setContentOffset: to scroll the table view, but this had the same effect.
The underlying issue here is that I was lazy and used CGAffineTransformMakeTranslation instead of manually computing the new frames, because I forgot that when the transform property is changed frames become "undefined" according to Apple documentation.
It ended up working okay until lots of messages came in at which point the table view's frame would get messed up and offset somehow which would cause the bottom of the table view to be displayed slightly lower than it should've been.
Lesson learned: Don't mix frame computations and transformations in one animation. Either keep everything in terms of frames, or keep everything in terms of transformations and use center + bounds.
Related
I have a UICollectionView which shows images retrieved from the web. They are downloaded asynchronous.
When user scrolls fast, they see placeholders until the cell loads. It seems UICollectionView only loads what is visible.
Is there a way to say "collection view, load 20 cells more above and below" so chance is higher that it loaded more cells while user was looking at content without scrolling?
The idea is to have the VC recognize when a remote load might be required and start it. The only tricky part is keeping the right state so you don't trigger too much.
Let's say your collection is vertical, the condition you want to know about is when:
BOOL topLoad = scrollView.contentOffset.y < M * scrollView.bounds.size.height
or when
BOOL bottomLoad = scrollView.contentOffset.y > scrollView.contentSize.height - M * scrollView.bounds.size.height
in other words, when we are M "pages" from the edge of the content. In practice though, this condition will be over-triggered, like when you're first loading, or if you're testing it on scrollViewDidScroll, you don't want to generate web requests for every pixel of user scrolling.
Getting it right, therefore, requires additional state in the view controller. The vc can have a pair of BOOLs, like topLoadEnabled, bottomLoadEnabled, that are NO until the view is ready. Then, scroll delegate code looks like this:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
// compute topLoad and bottomLoad conditions
if (topLoad && self.topLoadEnabled) [self startTopLoad];
similarly for bottom. The load code looks like this, abstractly:
self.topLoadEnabled = NO; // don't trigger more loading until we're done
[self.model getMoreTopStuff:^(NSArray *newStuff, NSError *error) {
// do view inserts, e.g. tableView.beginUpdates
self.topLoadEnabled = YES;
}];
Same idea for bottom. We expect the model to fetch for itself (maybe the model has image urls) and cache the result (then the model has images). As the datasource for the view, the view controller gets called upon to configure view cells. I can just naively ask the model for images. The model should answer either fetched images or placeholders.
Hope that makes sense.
In my opinion you are making the wrong assumption: cells are just views so you shouldn't treat them as model objects. UICollectionView and UITableView are very efficient because they constantly recycle cells so you should think in therms of pre loading content in the business side of things. Create interactor or viewmodel objects and populate your data source with those, then you'll be able to ask those objects to preload images, if you still wish to do so.
A BOOL flag seldom is the answer. I'd rather go for estimating a reasonable page size and fetching images as needed from the cellForItemAtIndePath method.
When a user adds an item to my list, I want to scroll to the new row, highlight it, and select it (which will push a new controller). The key part is waiting for the scroll animation to complete before pushing the new controller.
In this answer, I learned how to use the animation delegate to wait until the scroll is complete.
However, if the insertion row is already on scree, the table view will not scroll and the method will not fire.
How can I wait to push the new controller until the end of the scroll, and deal with the case where no scroll will be initiated - and how might I tell the difference between each case?
The easiest way to check whether a given row is visible in your table view is something like this:
if (!CGRectContainsRect([self.tableView bounds], [self.tableView rectForRowAtIndexPath:indexPath])
{
// the row is partially outside the table view’s boundaries and needs to be scrolled for full visibility
}
else
{
// the row is within the boundaries and does not need to be scrolled
}
Try creating a method to see if scrolling is needed. If no scrolling is needed, call the push right away, otherwise wait for the delegate call and push.
- (BOOL)isSrollingingNeededForIndexPath:(NSIndexPath *)indexPath {
NSArray *visibleIndices = [self.tableView indexPathsForVisibleRows];
for (NSIndexPath *visibleIndexPath in visibleIndices)
if ([indexPath compare:visibleIndexPath] == NSOrderedSame)
return NO;
return YES;
}
Edit: Good point. Since indexPathsForVisibleRows is used for data rendering.
You could do essentially the same thing with indexPathsForRowsInRect where you use the content.offset.y and the tableview.frame.size.height to determine your "visible rect".
Then to account for partially visible rows at the top and bottom you could add rowHeight-1 to the top of the rect and subtract rowHeight - 1 from the bottom of the rect. Code shouldn't be too gnarly if you have static height rows. If you have varying height rows it would still work, but it would be a bit more involved.
All said though, it seems like a lot of code for something which you'd think would have a simple answer.
I have a potentially very long scrolling log of messages within a UITableView The user would be able to navigate the table by scrolling.
What I'm trying to do is add new content at the top of the table. If the top of the table is visible, the new row would push the rest of the content down. But if the first row of the table is not visible, the table would silently add another row on top without scrolling.
I'm doing this to prevent the table scrolling to the top from interrupting the user browsing data. If I ask the table to simply scroll to the new insertion point, the user's position within the table would be lost, frustrating the user.
One of the ways I can try to implement this is by inserting data into an NSMutableArray at index 0. Would this work? An extra question: how expensive is inserting data at the beginning of an NSMutableArray. I would expect that such operation is optimized, but would like to make sure. What am I missing with this implementation?
Thank you!
How to insert without bumping the view:
Use the tableView's (UIScrollView) contentOffset values to find where the user is currently scrolled to.
Add the new row at the top, with animated:NO
Update the tableView's contentOffset to be where the user was at plus whatever the height of your row is.
How to avoid causing issues doing this while the user is dragging:
Keep track of when the user is dragging, and if you want to insert rows during a drag / motion, add them to a queue that is executed after the user releases.
CGSize beforeContentSize = self.tableView.contentSize;
[self.tableView reloadData];
CGSize afterContentSize = self.tableView.contentSize;
CGPoint afterContentOffset = self.tableView.contentOffset;
CGPoint newContentOffset = CGPointMake(afterContentOffset.x, afterContentOffset.y + afterContentSize.height - beforeContentSize.height);
self.tableView.contentOffset = newContentOffset;
Just posted an answer with code snippets here
Keep uitableview static when inserting rows at the top
Basically:
Save the scroll offset where you would have called beginUpdate:.
In place of calls to InsertCell you add the cell's height to the saved offset.
Where you would have called endUpdate:, call reloadData, then scroll by the saved offset with NO animation.
Could you please help me with circular scrolling in tableview please.
I want that if I scroll down tableview, the rows should go in the reverse way --
it should appear that move back around (bottom rows go around and now come back down from the top) i.e, cyclic scrolling basically.
How can I do so. Any suggestions please.
Thanx in advance.
You could "fake" the cyclic scrolling repeating the same cells all over again. In the numberOfRowsInSection method, return n times the actual number of rows. Make sure n is big enough.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return numberOfActualRows*100;
}
Then in the cellForRowAtIndexPath method (and elsewhere) use the mod operator (%) to return the proper cell.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSUInteger actualRow = indexPath.row % numberOfActualRows;
...
}
You may want to hide the sroll indicator.
self.tableView.showsVerticalScrollIndicator = NO;
You may also want to scoll the table view to the middle before you display the table so scrolling backwards works fine.
[self.tableView scrollToRowAtIndexPath: [NSIndexPath indexPathForRow:[self tableView:self.tableView numberOfRowsInSection:0]/2 inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:NO];
Of course, the user would eventually hit the bottom or the top if he/she kept scrolling over and over.
This question has already been asked: implementing a cyclic UITableView
I'm copying that answer here to make it easier because the asker hasn't ticked my answer.
UITableView is same as UIScrollView in scrollViewDidScroll method.
So, its easy to emulate infinite scrolling.
double the array so that head and tail are joined together to emulate circular table
use my following code to make user switch between 1st part of doubled table and 2nd part of doubled table when they tend to reach the start or the end of the table.
:
/* To emulate infinite scrolling...
The table data was doubled to join the head and tail: (suppose table had 1,2,3,4)
1 2 3 4|1 2 3 4 (actual data doubled)
---------------
1 2 3 4 5 6 7 8 (visualising joined table in eight parts)
When the user scrolls backwards to 1/8th of the joined table, user is actually at the 1/4th of actual data, so we scroll instantly (we take user) to the 5/8th of the joined table where the cells are exactly the same.
Similarly, when user scrolls to 6/8th of the table, we will scroll back to 2/8th where the cells are same. (I'm using 6/8th when 7/8th sound more logical because 6/8th is good for small tables.)
In simple words, when user reaches 1/4th of the first half of table, we scroll to 1/4th of the second half, when he reaches 2/4th of the second half of table, we scroll to the 2/4 of first half. This is done simply by subtracting OR adding half the length of the new/joined table.
Written and posted by Anup Kattel. Feel free to use this code. Please keep these comments if you don't mind.
*/
-(void)scrollViewDidScroll:(UIScrollView *)scrollView_
{
CGFloat currentOffsetX = scrollView_.contentOffset.x;
CGFloat currentOffSetY = scrollView_.contentOffset.y;
CGFloat contentHeight = scrollView_.contentSize.height;
if (currentOffSetY < (contentHeight / 8.0)) {
scrollView_.contentOffset = CGPointMake(currentOffsetX,(currentOffSetY + (contentHeight/2)));
}
if (currentOffSetY > ((contentHeight * 6)/ 8.0)) {
scrollView_.contentOffset = CGPointMake(currentOffsetX,(currentOffSetY - (contentHeight/2)));
}
}
P.S. - I've used this code on one of my apps called NT Time Table (Lite). If you want the preview, you can check out the app: https://itunes.apple.com/au/app/nt-time-table-lite/id528213278?mt=8
If your table can sometimes be too short, at the beginning of the above method you can add a if logic to exit when data count is say for example less than 9.
I haven't done this myself, but you could try the approach you'd use with a UIScrollView to implement cycling scrolling of views (after all UITableView is a subclass of UIScrollView).
I would do as follows:
Create a UITableView with an arbitrary number of cells (at least 7 but will need more to prevent fast scrolling bumping at the end)
Position your UITableView so the centre cell is visible
Maintain a pointer to the index of the cell you are looking to display
In your cellForRowAtIndexPath: use your pointer to as an offset and add the row to it to get the cell that you want
When the UITableView has stopped moving (your UITableViewDelegate can serve as UIScrollViewDelegate so you can use scrollViewDidEndDecelerating). Set your offset index to the current cell, move the table view back to the centre cell without animation and reload the data.
The issue you will have is if the user keeps scrolling without stopping they will eventually hit the bumpers as the number of cells in the table is reached.
Hope this helps, and please post back if you get this working and it looks at all reasonable.
Regards
Dave
This is very much difficult to implement. However, take a look at the ScorllingMadness, which shows the demo of nested (cyclic) pages in a scroll-view.
You need to use the similar kind of trick here as UITableView is a subclass of UIScrollView.
i am getting images with messages from .net webserver by giving xml input.
that's working fine.
i am sending request for every 3 sec and if any new messages and images are there i just add those messages and images to the array and reload the table view.
That is also fine,But what i need is when i reload table view when ever there is new messages that will be displayed on table view by smooth scrolling the existing row.
same as twitter.
can any one please help me.
Thank u in advance.
if you know the indexPath of newly added row , you could use the below function of UITableView to scroll upto new row with animation (smooth effect).
- (void)scrollToRowAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UITableViewScrollPosition)scrollPosition animated:(BOOL)animated
Instead Of completely reloading the table, you should call insertRowsAtIndexPaths:withRowAnimation:
The second option will let you specify how you want it animated
(this will also be much more efficient than reloading all the data every time)
You can do something like the following:
(this makes the assumptions that recievedImageFrom server will be called after the data has already been loaded into the data source object. Also, myDataSourceArray is the array where the data is being stored for the table and myTableView is the UITableView.
-(void)recievedImageFromServer
{
NSUInteger row = [myDataSourceArray count]-1; // This can also be set to 0 if you want
// to insert at the top of the table
NSIndexPath* newIndex = [NSIndexPath indexPathWithIndex:row];
[myTableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndex]
withRowAnimation:UITableViewRowAnimationTop];
}
It will then request the cell from its data source.