I use UICollectionView + AFNetworking 2 for loading images asynchronously from a webservice. It's working great. However, how do I implement automatic scrolling (when user reaches bottom of page, or close to it, the API is called to fetch more cells).
I've seen lots of examples but there doesn't seem to be a best practice. For example, one tutorial suggested scrollViewDidScroll, but that's called every time the view is scrolled. Is that the correct implementation?
PS. I'm coding in Swift.
Here's my cell creation code:
func collectionView(collectionView: UICollectionView!, cellForItemAtIndexPath indexPath: NSIndexPath!) -> UICollectionViewCell! {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(cellIdentifier, forIndexPath: indexPath) as UICollectionViewCell
let offer = self.offers[indexPath.row]
var cellImageView = cell.viewWithTag(100) as UIImageView
let requestURL = NSURLRequest(URL: offer.largeImageURL)
cellImageView.setImageWithURLRequest(
requestURL,
placeholderImage:placeholderImage,
success: {
(request:NSURLRequest!, response:NSHTTPURLResponse!, image:UIImage!) in
cellImageView.image = image
},
failure: {
(request:NSURLRequest!, response:NSHTTPURLResponse!, error:NSError!) in
NSLog("GET Image Error: " + error.localizedDescription)
}
)
return cell
}
You could do it in a scrollViewDidScroll delegate method or collectionView:cellForItemAtIndexPath: (here you could detect that last cell is loading). What you would need to do is to create a wrapper for AFNetworking call to prevent it from loading the data few times (some kind of flag that indicates that it's already loading).
After that you could insert additional cell with some kind of activity indicator at the bottom and remove it after AFNetworking call is finished.
You should use the willEndDragging UIScrollview delegate method to determine where the scrollview will stop scrolling.
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
This has several advantages over the other methods:
didScroll will be called for every time the scrollview scrolls, even fractionally, and even when you trigger it from code. I've seen didScroll get called even with a bounds change, like when the orientation changes, especially on iPad. It just gets called too often!
didEndDecelerating may not get called if the scrollview comes to a sudden stop - like when the user presses and hold the scrollview while it's slowing
willEndDragging will only be called when the user scrolls your scroll view, not by code / layout changes.
In the willEndDragging, use the target content offset to determine if you need to load more results. Once your API has finished fetching data, simply call reloadData on the collection view and the new results will be shown. Note that the target contentOffset is a pointer, so you'll have to use targetContentOffset->y to get the target y position.
The actual code to do this will be implementation dependent so I'm not putting in any sample, but it should be simple enough to code up.
Yes, use scrollViewDidScroll to check that you are at the bottom, than do any work you need (place loader, load data and so on).
You can use ready component SVInfiniteScrolling (which is part of SVPullToRefresh). Or look at its implementation to be inspired.
Happy coding!
I'm using this code in my project. I wrote it with Obj-C and don't know enough to convert it to Swift. So, if you can convert, you can detect yourself if user reach end of the page like that;
- (BOOL)detectEndofScroll
{
BOOL scrollResult = NO;
CGPoint offset = _collectionView.contentOffset;
CGRect bounds = _collectionView.bounds;
CGSize size = _collectionView.contentSize;
UIEdgeInsets inset = _collectionView.contentInset;
float yAxis = offset.y + bounds.size.height - inset.bottom;
float h = size.height;
if(yAxis+14 > h)
{
scrollResult = YES;
}
else
{
scrollResult = NO;
}
return scrollResult;
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
if ([self detectEndofScroll])
{
// load more data
}
}
Related
I'd like to implement a "zoom" effect on a paging UIScrollView that I've created, but I am having a lot of difficulty. My goal is that as a user begins to scroll to the next page, the current page zooms out to become a little bit smaller. As the next page comes into view, it zooms in until it becomes its full size. The closest thing I could find to an example was this...
https://www.pinterest.com/pin/147141112804210631/
Can anyone give me some pointers on how to accomplish this? I've been banging my head against a wall for the last 3 days on this.
I would recommend using the scrollView.contentOffset.y of your paginated UIScrollView to keep track of the scroll and to use that value to animate the transform of your views inside the UIScrollView.
So add your paginated scrollview and make self as delegate.
paginatedScrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, [[self view] bounds].size.width, [[self view] bounds].size.height-paginatedScrollViewYOffset)];
[self.view addSubview:paginatedScrollView];
paginatedScrollView.pagingEnabled = YES;
[paginatedScrollView setShowsVerticalScrollIndicator:NO];
[paginatedScrollView setShowsHorizontalScrollIndicator:NO];
[paginatedScrollView setAlwaysBounceHorizontal:NO];
[paginatedScrollView setAlwaysBounceVertical:YES];
paginatedScrollView.backgroundColor = [UIColor clearColor];
paginatedScrollView.contentSize = CGSizeMake([[self view] bounds].size.width, [[self view] bounds].size.height*2); //this must be the appropriate size depending of the number of pages you want to scroll
paginatedScrollView.delegate = self;
Then use the delegate method scrollViewDidScroll to keep track of the scrollView.contentOffset.y
- (void) scrollViewDidScroll:(UIScrollView *)scrollView {
NSLog(#"Scroll Content Offset Y: %f",scrollView.contentOffset.y);
//use here scrollView.contentOffset.y as multiplier with view.transform = CGAffineTransformMakeScale(0,0) or with view.frame to animate the zoom effect
}
Use this Code scrollview its zoom in when scroll next page, the code is given below,
-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
GridCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"CollectCell" forIndexPath:indexPath];
cell.myscrollview.minimumZoomScale = 5.0;
cell.myscrollview.zoomScale = 5.0;
cell.myscrollview.contentSize = cell.contentView.bounds.size;
return cell;
}
if you change the zoom scale value its automatically zoom in or zoom out to be showed when scroll next or previous page.
hope its helpful.
I actually just posted an answer to a very similar question, where somebody tried to achieve this effect using a UICollectionView. The link to my answer is here: https://stackoverflow.com/a/36710965/3723434
Relevant piece of code I will post here:
So another approach would be to to set a CGAffineTransformMakeScale( , ) in the UIScrollViewDidScroll where you dynamically update the pages' size based on their distance from the center of the screen.
For every page, calculate the distance of its center to the center of yourScrollView
The center of yourScrollView can be found using this nifty method: CGPoint point = [self.view convertPoint:yourScrollView.center toView:*yourScrollView];
Now set up a rule, that if the page's center is further than x away, the size of the page is for example the 'normal size', call it 1. and the closer it gets to the center, the closer it gets to twice the normal size, 2.
then you can use the following if/else idea:
if (distance > x) {
page.transform = CGAffineTransformMakeScale(1.0f, 1.0f);
} else if (distance <= x) {
float scale = MIN(distance/x) * 2.0f;
page.transform = CGAffineTransformMakeScale(scale, scale);
}
What happens is that the page's size will exactly follow your touch. Let me know if you have any more questions as I'm writing most of this out of the top of my head).
I've done some work on stylized app guide page before.
For Me, I would use CADisplayLink to track the contentOffset.x of the scrollView, associate the value with your animation process. Don't put your views on the scrollView, put them on an overlay view of this scrollView.
This solution follows the philosophy: Fake it before you make it.
Based on CADisplayLink and physics simulation of UIScrollView, you will get smooth animation. Believe me.
What you really want isn't a UIScrollView, it's a UICollectionView with a custom layout. UICollectionViewLayoutAttributes has a transform property that you can set.
Say for example, in layoutAttributesForElementsInRect::
override func layoutAttributesForElementsInRect(rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let attributes = super.layoutAttributesForElementsInRect(rect) else {
return nil
}
return attributes.map { attribute -> UICollectionViewLayoutAttributes in
if attribute.frame.origin.y < 0 {
let scale = -attribute.frame.origin.y / attribute.frame.height
attribute.transform = CGAffineTransformMakeScale(scale, scale)
}
return attribute
}
}
Here, you're filtering by if the element is on the screen (so non-visible elements won't be counted) and checking to see if the y offset is less than 0. If it is, you take the difference between the negated y value and the item's height and turn that into a proportional scale.
You can do it however you want, if you want the scale to be between 1 and 0.5 for example. I like this way of doing things over mucking around with a scroll view.
I am implementing an infinite-scrolling calendar. My issue is that I would like to set the current month as the title in the navigation bar and it should update while scrolling - once you pass the section header view the title should update in the nav bar.
A possible solution would be to set the view title in the method called - (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath so that, when I calculate a new section Header, it also updates the title. The problem with this is that the title changes when the new section is at the bottom of the page.
Is there a way to know the "current section" of UICollectionView once the user has scrolled to it? Or can you think of a way to improve my current solution?
To help the readers of this post, I posted my own sample code for this question at this GitHub repo.
I have been pondering an algorithm that would allow you to know when the user has scrolled past a section header in order to update the title, and after some experimentation I have figured out how to implement the desired behavior.
Essentially, every time the scroll position changes you need to know what section the user is on and update the title. You do this via scrollViewDidScroll on the UIScrollViewDelegate - remembering a collection view is a scroll view. Loop over all the headers and find the one that's closest to the current scroll position, without having a negative offset. To do that, I utilized a property that stores an array of each section header's position. When a header is created, I store its position in the array at the appropriate index. Once you've found the header that's closest to your scroll position (or the index location of said header), simply update the title in the navigation bar with the appropriate title.
In viewDidLoad, fill the array property with NSNull for each section you have:
self.sectionHeaderPositions = [[NSMutableArray alloc] init];
for (int x = 0; x < self.sectionTitles.count; x++) {
[self.sectionHeaderPositions addObject:[NSNull null]];
}
In collectionView:viewForSupplementaryElementOfKind:atIndexPath:, update the array with the position of the created header view:
NSNumber *position = [NSNumber numberWithFloat:headerView.frame.origin.y + headerView.frame.size.height];
[self.sectionHeaderPositions replaceObjectAtIndex:indexPath.section withObject:position];
In scrollViewDidScroll:, perform the calculations to determine which title is appropriate to display for that scroll position:
CGFloat currentScrollPosition = self.collectionView.contentOffset.y + self.collectionView.contentInset.top;
CGFloat smallestPositiveHeaderDifference = CGFLOAT_MAX;
int indexOfClosestHeader = NSNotFound;
//find the closest header to current scroll position (excluding headers that haven't been reached yet)
int index = 0;
for (NSNumber *position in self.sectionHeaderPositions) {
if (![position isEqual:[NSNull null]]) {
CGFloat floatPosition = position.floatValue;
CGFloat differenceBetweenScrollPositionAndHeaderPosition = currentScrollPosition - floatPosition;
if (differenceBetweenScrollPositionAndHeaderPosition >= 0 && differenceBetweenScrollPositionAndHeaderPosition <= smallestPositiveHeaderDifference) {
smallestPositiveHeaderDifference = differenceBetweenScrollPositionAndHeaderPosition;
indexOfClosestHeader = index;
}
}
index++;
}
if (indexOfClosestHeader != NSNotFound) {
self.currentTitle.text = self.sectionTitles[indexOfClosestHeader];
} else {
self.currentTitle.text = self.sectionTitles[0];
}
This will correctly update the title in the nav bar once the user scrolls past the header for a section. If they scroll back up it will update correctly as well. It also correctly sets the title when they haven't scrolled past the first section. It however doesn't handle rotation very well. It also won't work well if you have dynamic content, which may cause the stored positions of the header views to be incorrect. And if you support jumping to a specific section, the user jumps to a section whose previous section's section header hasn't been created yet, and that section isn't tall enough such that the section header is underneath the nav bar (the last section perhaps), the incorrect title will be displayed in the nav bar.
If anyone can improve upon this to make it more efficient or otherwise better please do and I'll update the answer accordingly.
change below line in method viewForSupplementaryElementOfKind :
self.title = [df stringFromDate:[self dateForFirstDayInSection:indexPath.section]];
to this:
if(![[self dateForFirstDayInSection:indexPath.section-1] isKindOfClass:[NSNull class]]){
self.title = [df stringFromDate:[self dateForFirstDayInSection:indexPath.section-1]];
}
Hope it will help you.
Yes, the problem is that footer and header are not exists in visibleCells collection. There is other way to detect scroll for section header/footer. Just add a control there and find the rect for it. Like this:
func scrollViewDidScroll(scrollView: UIScrollView) {
if(footerButton.tag == 301)
{
let frame : CGRect = footerButton.convertRect(footerButton.frame, fromView: self.view)
//some process for frame
}
}
No solution here is fulfilling, so I came up with my own that I want to share, fully. If you use this, you have to make some pixel adjustments, though.
extension MyViewControllerVC: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == self.myCollectionView {
let rect = CGRect(origin: self.myCollectionView.contentOffset, size: self.cvProductItems.bounds.size)
let cellOffsetX: CGFloat = 35 // adjust this
let cellOffsetAheadY: CGFloat = 45 // adjust this
let cellOffsetBehindY: CGFloat = 30 // adjust this
var point: CGPoint = CGPoint(x: rect.minX + cellOffsetX, y: rect.minY + cellOffsetAheadY) // position of cell that is ahead
var indexPath = self.myCollectionView.indexPathForItem(at: point)
if indexPath?.section != nil { // reached next section
// do something with your section (indexPath!.section)
} else {
point = CGPoint(x: rect.minX + cellOffsetX, y: rect.minY - cellOffsetBehindY) // position of cell that is behind
indexPath = self.myCollectionView.indexPathForItem(at: point)
if indexPath?.section != nil { // reached previous section
// do something with your section (indexPath!.section)
}
}
}
}
}
UICollectionView inherits UIScrollView, so we can just do self.myCollectionView.delegate = self in viewDidLoad() and implement the UIScrollViewDelegate for it.
In the scrollViewDidScroll callback we will first get the point of a cell below, adjust cellOffsetX and cellOffsetAheadY properly, so your section will be selected when the cell hits that point. You can also modify the CGPoint to get a different point from the visible rect, i.e for x you can also use rect.midX / rect.maxX and any custom offset.
An indexPath will be returned from indexPathForItem(at: GCPoint) when you hit a the cell with those coordinates.
When you scroll up, you might want to look ahead, possibly ahead your UICollectionReusableView header and footer, for this I also check the point with negative Y offset set in cellOffsetBehindY. This has lower priority.
So, this example will get the next section once you pass the header and the previous section once a cell of the previous section is about to get into view. You have to adjust it to fit your needs and you should store the value somewhere and only do your thing when then current section changes, because this callback will be called on every frame while scrolling.
I have an UICollectionView in which each cell contains a PFObject. As there are potentially hundreds of these objects for each UICollectionView, I don't want to query all the objects at once, and instead only call a limited amount, and then automatically call for more as the user scroll towards the end (somewhat like endless scrolling that many web apps use).
Would PFQuery support these type of calls, and if so how would I be able to call continuously and automatically?
Thanks!
Perhaps something like this to get you on the right track:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
float scrollViewHeight = scrollView.frame.size.height;
float scrollContentSizeHeight = scrollView.contentSize.height;
else if (scrollView.contentOffset.y <= 0) {
// then we are at the top
}
else if (scrollView.contentOffset.y + scrollViewHeight >= scrollContentSizeHeight) {
// then we are at the bottom - query new results
}
}
I don't think you'd have to add anything for it to call as long as your controller is already your collectionView's delegate.
I want to change the height of my UITableView based on the scrolled content. Right now I do it by getting the scrollViewDidScroll event and then getting scrolled value and then change the height. Here is my code (for simplification I omitted the irrelevant code):
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
float currentOffset = scrollView.contentOffset.y
double delta = currentOffset - storedOffset;
if(delta != 0)
{
delta = abs(delta);
CGRect newListFrame = myTableView.frame;
float newListHeight = newListFrame.size.height + delta;
newListFrame.size.height = newListHeight;
myTableView.frame = newListFrame;
}
storedOffset = currentOffset;
}
But this approach is wrong because with this approach my UITableView's content is scrolled only a little bit and that's not what I want. I just want to get the value of that list that would be scrolled without actually scrolling it. Is there any way to do that? I thing I could get raw finger moved event but can I get it on UITableVIew? Can I do something like this using a UITableView method?
Is it possibile add UIRefreshControl at the bottom of the UITableView?
I would use it to load more data.
Please, Any suggest?
I believe there won't be any simple solution to this problem. May be someone could write a separate library to implement this behaviour plus it will cause more complications once you cache data in tableview.
But let us break down the problem and that might help you in achieving what you want. I have used the following code to add more rows in the app:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
float endScrolling = scrollView.contentOffset.y + scrollView.frame.size.height;
if (endScrolling >= scrollView.contentSize.height)
{
NSLog(#"Scroll End Called");
[self fetchMoreEntries];
}
}
Or you could do something like that:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if ((scrollView.contentOffset.y + scrollView.frame.size.height) >= scrollView.contentSize.height)
{
if (!self.isMoreData)
{
self.MoreData = YES;
// call some method that handles more rows
}
}
}
You must have seen such methods in many question as i have described above and certainly not what you have asked for. But what you can do is while the above code in in process to load more data, you can add a subview and show a couple of images similar to what UIRefreshControl offers. Add the images in the subview to be shown as a progress until the above code gets executed. Home this helps.
By the way, i will suggest you not to do that as it will just waste your time for making something so smaller unless you are just doing it for learning purposes.
The following answer is in Xcode 8 and Swift 3.
first declare
var spinner = UIActivityIndicatorView()
than in viewDidLoad method write the following code:
spinner = UIActivityIndicatorView(activityIndicatorStyle: .whiteLarge)
spinner.stopAnimating()
spinner.hidesWhenStopped = true
spinner.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: 60)
tableView.tableFooterView = spinner
now finally override the scrollViewDidEndDragging delegate method with following:
override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
let offset = scrollView.contentOffset
let bounds = scrollView.bounds
let size = scrollView.contentSize
let inset = scrollView.contentInset
let y = offset.y + bounds.size.height - inset.bottom
let h = size.height
let reloadDistance = CGFloat(30.0)
if y > h + reloadDistance {
print("fetch more data")
spinner.startAnimating()
}
}
You can use UIView to customize your refreshControl at bottom. Create UIView and add it to UITableView as footerView.
UIView* footerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 50)];
[footerView setBackgroundColor:[UIColor colorWithPatternImage:[UIImage imageNamed:#"refreshImage.png"]]];
tableView.tableFooterView = footerView;
Hide it: tableView.tableFooterView.hidden = YES;
Implement UIScrollView delegate -
(void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if ((scrollView.contentOffset.y + scrollView.frame.size.height) >= scrollView.contentSize.height)
{
tableView.tableFooterView.hidden = NO;
// call method to add data to tableView
}
}
before adding data to tableView save current offset by CGPoint offset = tableView.contentOffset;
call reloadData then set previously saved offset back to tableView [tableView setContentOffset:offset animated:NO];
So that you can feel new data added at bottom.
I've been stuck on the same problem recently and write a category for UIScrollView class. Here it is CCBottomRefreshControl.
Actually the free Sensible TableView framework does provide this functionality out of the box. You specify the batch number and it will automatically fetch the data, displaying the last cell as a 'load more' cell. Once you click that cell, it will automatically load the next batch, and so on. Worth taking a look at.
For UITableView I suggest the following approach:
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
// if this is the last row that we have in local data
// and we didn't reach the total count of rows from the server side
if (count == indexPath.row+1 && count < total)
{
// .. fetch more rows and reload table when data comes
}
}
I didn't use the scroll view methods intentionally because the scroll view is slow. It happens that scroll view scrolls, we request more data, refresh more rows, and scroll view is not yet finished with scrolling, it is still decelerating and causing problems with fetching more data.
This Swift library add pull to refresh to the bottom of UITableview.
https://github.com/marwendoukh/PullUpToRefresh-iOS
I hope this help you.
Use CCBottomRefreshControl in cocoapods,
As it is for language objective.
you can add the pod file to your swift project then run it,
i have done it, no problem occurs for me
Hope it helps.