I have a UITableview that is populated by a REST web service. I have bouncing enabled so that I can trigger a call to the server with
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
Using this method the call gets triggered when the user lifts their finger after trying to scroll past the bottom of the tableView. The view then bounces back and the new cells are added. The problem I have is that it is not obvious anything was added. The tableView bounces back to the bottom of the screen and it isn't until the user tries to scroll again that the new cells become visible. Here is the method I use to trigger the call.
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
NSInteger currentOffset = scrollView.contentOffset.y;
NSInteger maximumOffset = scrollView.contentSize.height - scrollView.frame.size.height;
if (maximumOffset - currentOffset <= -40)
{
if (scrollView == _feedTableView)
{
//This method has a call to the server and adds the new cells to the table
[self loadFeed:feedRefreshControl];
}
}
}
I tried turning off bouncing after the trigger in an attempt to get the bottom cell to remain where it was when the user lifts their finger but the last cell just shoots back to the bottom in an non-animated way.
Any ideas would be appreciated!
In the past, I've used a fake last cell which held a spinner. As the user scrolls down to the end of the list, they see the spinner cell as the last cell. When more data is added the spinner cell gets replaced with the first cell of the new data. This is an obvious visual clue to the user that more information has been loaded into the list. I also flashed the status bar, again just a visual cue to the user that the data has been updated.
Related
I've got a view that has 3 tableView's. One is the "Main Table View", and then I have an 'Answers Table View' and 'Percentage Table View'.
When the screens loads, the Main Table View occupies the top 95% of the screen. The bottom of the screen is a UIView containing 2 buttons. "Answers" and "Percentage".
The way it works, is if I click "Percentage" it changes the height of the Main Table View to 0, and gives that height to the Answers Table View. This animates the "Answers/Percentage" View to the top, and reveals either the Answers or Percentage TableView below it.
Here's an example:
As you can see, I click on "Percentage" which animates it up. If you click on "Percentage" again it animates it back down.
However, what I want to do is if the "Answers/Percentage" view is at the bottom of the View, and the user scrolls the Main Feed UP reaches the very end of the tableView's contents (not just the end, but the end and a little bit more), I want to animate it up like in the .gif.
Similarly, if the "Answers/Percentage" is at the top, and the user scrolls the lower "Answers Table View" down past a certain point where there is no more data above, it will completely animate.
Also, I do not ever want the "Answers/Percentage" view to be in the middle, and showing a tableview both top and bottom. All one, or the other, but not a bit of both. Which I have right now.
What I need to know is... how can I detect if the user has scrolled past the very top or very bottom of the table view +30 pixels for example, to initiate my animation?
You can use the contentOffset property of the table view.
if(tableView.contentOffset.y >= (tableView.contentSize.height - tableView.frame.size.height)) {
// Start the animation
}
I haven't tested this, but let me know if it works.
Making another answer based on sublimepremise's approach because it's easier to format and post code that way. The base idea is to check the table view's .contentOffset.y, e.g. by implementing scrollViewDidScroll in its delegate, and triggering your animations accordingly.
It may have a bit of a QnD feel to it, but if you need to also abort the user's dragging action when triggering the animation, an easy way to do so would be by "resetting" the table view's gesture recognizer. In code that could look something like this:
static const CGFloat kChrisTableViewAnimationThreshold = 30.0f;
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView == self.topTableView) {
if([self scrolledPastBottomThresholdInTableView:self.topTableView]) {
// Start the animation
// ...
// Toggling the table view's pangGestureRecognizer off and on cancels the gesture
self.topTableView.panGestureRecognizer.enabled = NO;
self.topTableView.panGestureRecognizer.enabled = YES;
}
}
}
- (BOOL)scrolledPastBottomThresholdInTableView:(UITableView *)tableView {
return (tableView.contentOffset.y - kChrisTableViewAnimationThreshold >= (tableView.contentSize.height - tableView.frame.size.height));
}
Note that this illustration obviously only covers recognizing when the top table view is scrolled past its bottom end, but adapting it for handling other scenarios should be pretty straightforward.
is there any way to do like Facebook loading that i have one array having 100 objects and on screen 10 cell display.what i want is when i scroll want loading at footer and another 10 data should come and so on...i tried too much but couldn't find this.
Data is come from WebService and i am taking it to an array.
Also is this possible that when i scroll and goes to bottom of the tableview it automatically display 10 new cell without reload all table.
i tried this but not success
if (indexPath.row == [newsArray count] - 1)
[self News_WS_Called];
I achieved this by using UIScrollViewDelegate's scrollViewDidScroll method. I basically see if the view is scrolled to the bottom and then call the method that handles fetching more, appending to data source and reloading the tableview. Here's a very basic example:
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGRect mainRect = [[UIScreen mainScreen] bounds];
if(scrollView.contentOffset.y > scrollView.contentSize.height - mainRect.size.height){
// at the bottom of the scroll view
[self News_WS_Called];
}
}
Something things to consider is that you will need some kind of mutex so that the reload / fetch code does not repeatedly get called or gets called when there is nothing on the table view.
I'm racking my head over this one, I know this has to be possible but after going over every bit of documentation I still can't come up with something that works well.
Basically, I have a UICollectionView in the LAST cell of a UITableView. What I want to happen is, only when the UITableView is totally scrolled to the bottom, can the UICollectionView in it's last cell start scrolling. And, if the TableView offset reaches the bottom of the CollectionView's tableviewcell during a drag / pan, any additional dragging of the current table view drag / pan should effect the collection view instead of the table view.
Also, when the collection view is scrolling, if a user starts scrolling on the collection view, if the collection view reaches the top of it's scroll (Content Y offset of 0 or less), and additional scrolling of the current pan/drag gesture in affect should cause the containing table view to scroll up.
The reason I want to achieve this effect, is because the table view cell above the last cell containing the collection view, contains a UISegmentControl that toggles the contents of the UICollectionView, and I want the user to be able to toggle this segment at any time while scrolling in the CollectionView. Meaning the collection view has to scroll but the parent table view needs to not scroll..
I've tried playing with the gesture recognizers, and doing something with
– gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:
This gets me halfway there, I can use a flag to return yes/no to this method if the collection view is to scroll into a negative offset (Past it's top offset) or if the table view has reached the end of it's total scroll.. I get sort of close to what I want to achieve, but if I scroll up slowly on the collection view, it doesn't fire the simultaneous recognizer, same happens if I scroll down on the table view too slowly.
Another issue is, I do NOT want the collection view to bounce. However setting bounces to NO totally prevents the simultaneous recognizer to fire at all. I even tried setting content offset to CGPointZero on the collection view in it's viewDidScroll if it's y offset were to dip below zero. This also doesn't work and prevents the simultaneous method from firing...
Does anyone have any idea what to do? Or something to point me in the right direction?
UPDATE -
Still trying at this, I've made little progress towards the behavior I'm trying to achieve. I've messed with toggling userInteractionEnabled in the viewDidScroll method, as have I tried in willBeginDragging. The same with scrollEnabled property.. No luck :( I get a behavior similar to what I want with this, however the parent view will not scroll up until the user lets off the screen and attempts to scroll again..
UPDATE -
Is there anyway to transition the panGestureRecognizer currently handling scroll events DURING scrolling? If I could transition the scroll handler from the child to the parent while still scrolling this would solve my issue. I've looked through apple's gesture related and uiscrollview related documentation and can't find anything close to doing that.
UPDATE -
Just got done trying something like this..
- (CGPoint)maxParentContentOffset
{
return CGPointMake(0, self.parentScrollView.contentSize.height - self.frame.size.height - 44);
}
- (void)parentScrollViewDidScroll:(UIScrollView *)parentScrollView
{
if (self.contentOffset.y > 0) {
self.parentScrollView.contentOffset = [self maxParentContentOffset];
}
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (scrollView.contentOffset.y < 0) {
scrollView.contentOffset = CGPointZero;
}
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
CGPoint translation = [scrollView.panGestureRecognizer velocityInView:scrollView.superview];
if (translation.y < 0) {
[UIView animateWithDuration:0.5f animations:^(void) {
self.parentScrollView.contentOffset = [self maxParentContentOffset];
}];
}
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return YES;
}
However there is a problem... for some reason I keep getting a bad access error on the following method
- (void)parentScrollViewDidScroll:(UIScrollView *)parentScrollView
{
if (self.contentOffset.y > 0) {
self.parentScrollView.contentOffset = [self maxParentContentOffset];
}
}
Specifically setting the content offset. Which is really strange, because with breakpoints I'm seeing parentScrollView and self as being set. I'm wondering if it's not a bad access but it's getting trapped in an infinite loop for some reason? Any ideas?
Even though you may manage to make it work now, embedding a collection view inside a table view (both UIScrollView subclasses) is not a good idea and it will bug as soon as apple modifies their implementation.
Try to migrate to a single UICollectionView layout. After all there's nothing you can't achieve with a collection view that a table view can.
Separate your "table view" and "collection view" in two (or more) collection view sections, then implement layoutAttributesForItemAtIndexPath: differently according to the indexPath.section.
To make it table view-like you'll want to return frames whose width are the same as the collection view.
If your layout is simpler then you could use a UICollectionViewFlowLayout (maybe your already are) and implement collectionView:layout:sizeForItemAtIndexPath: as described above.
Figured this out after a good 8 hours.. I had a confliction due to infinite setting of the parent scroll view offset, since I had multiple objects that were of the same class that received a call whenever their parent view scrolled, both trying to set the same parent view offset to zero, which caused the other class to see scroll changing, and calling their method to change offset, and the process happening infinitely causing a crash.
This code however, solved everything and functions exactly as I was desiring. Hopefully this helps anyone else in the future looking to get congruent scrolling between a parent and child scroll view.
- (CGPoint)maxParentContentOffset
{
return CGPointMake(0, self.parentScrollView.contentSize.height - self.frame.size.height - 44);
}
- (void)parentScrollViewDidScroll:(UIScrollView *)parentScrollView
{
if (self.contentOffset.y > 0 && self.isDragging) {
self.parentScrollView.contentOffset = [self maxParentContentOffset];
}
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (scrollView.contentOffset.y < 0) {
scrollView.contentOffset = CGPointZero;
}
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
CGPoint translation = [scrollView.panGestureRecognizer velocityInView:scrollView.superview];
if (translation.y < 0) {
[UIView animateWithDuration:0.5f animations:^(void) {
self.parentScrollView.contentOffset = [self maxParentContentOffset];
}];
}
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return YES;
}
In an application that I've worked, we needed something close to that, we wanted to have a uitableview inside the first cell of another uitableview, which should be scrollable (you can check the app here)
Our approach was quite different, and we thought about doing it your way, but that way was a bit overkill for the time we had, and needed too much custom control instead of using what apple already gives us.
I'll share our method just for you to have another way to solve your problem, since I can't figure out what is wrong in that method call.
On the outer tableview's datasource lifecycle, we were able to know how our inner table view would be, allowing us to feed our inner table view with all the data, which would then gave us the total content size of the inner table view.
With this info, and since we knew (and in your case, you know) the actual cell where the inner table view would be placed, we make that cell's height equal to the height of the inner tableview content size. With this, the inner table view is 'totally visible'(in your case, the collection view) inside the outer table view cell, but since the cell is so big, it looks like we are scrolling the inner table view when we are simply scrolling a really big, special, cell.
And it won't bounce, if you don't want it too.
In our case, this worked, and the implementation was pretty straight forward.
In your case, and since you have a UISegmentedController, you should do a beginUpdates endUpdates and a 'reloadData', upon press in the UISegmenedController, in your outer table view in order to be able to recalculate the collection view's content size and resize the last cell, but that is pretty straight forward.
I know I'm not answering your question, but this method allowed us to achieve something so similar in such a fast and straight forward way that I thought it was worth sharing.
Hope it helps.
I have a uitableview where each cell has a uitextfield. If i touch a text field low down in the table view, then it sets the text field as first responder and scrolls to it, and also scrolls up a keyboard (all as expected). But if the keyboard animation is faster than the table view scrolling, then the keyboard momentarily covers up the text field, which then therefore resigns as first responder.
It's hard to reproduce with a basic project or any code snippet that i could paste here, because the problem is that each cell is doing a lot of layout, and it's dequeueing other cells as it scrolls up etc, meaning that the scrolling is fairly slow.
I don't mind the slightly jerky scrolling, i'm just interested in finding a way of ensuring the text field keeps itself as first responder even when it momentarily is hidden by the keyboard.
edit:
as in the answer below, i'm now trying to maintain whether or not the table view is scrolling, and only allow resigning of first responder if scrolling = NO. The code for UITableViewDelegate is below, but the problem now is that non animated movements of the table view (i.e. scrollToRowAtIndexPath...animated:NO) result in scrollViewDidScroll, but not scrollViewDidEndScrollingAnimation. Any ideas on how to get round this in a way that isn't even more clunky?
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
NSLog(#"YES");
self.scrolling = YES;
}
-(void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
NSLog(#"NO");
self.scrolling = NO;
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
NSLog(#"NO");
self.scrolling = NO;
}
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView
{
NSLog(#"NO");
self.scrolling = NO;
}
You could subclass UITextField and implement
- (BOOL)resignFirstResponder
There you could return NO while scrolling.
im atempting to do a facebook type load more data and right now it works, but very laggy on the device because its asking the server to get anything that isnt there already, then calling a scroll all the way down function (because somehow when i reload the data it scrolls to the top). If there would be a way to prevent scrolling to the top that would be great. But my main thing is, is there a way to detect when i scrolled down, and LET GO (stopped scrolling) as in i scrolled past what i have, then it went back to its possition and then calls my methods... Currently it keeps getting called when the scroll is greater then the height of the UITableView heres the code
-(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
CGFloat height = scrollView.frame.size.height;
CGFloat contentYoffset = scrollView.contentOffset.y;
CGFloat distanceFromBottom = scrollView.contentSize.height - contentYoffset;
if(distanceFromBottom < height)
{
[getMessage removeAllObjects];
[self loadMessages];
[self.tableView reloadData];
[self scrollAllTheWayDown];
}
}
UIScrollViewDelegate method
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;
does what you want. it gets called after user stops scrolling, and you can check for scrollOffset at that point to see if you should trigger your refresh code. (you'd use scrollViewDidScroll to update the view to show user update will happen if he lets go)