UIScrollView, reaching the bottom of the scroll view - iphone

I know the Apple documentation has the following delegate method:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView; // called when scroll view grinds to a halt
However, it doesn't necessarily mean you are at the bottom. Cause if you use your finger, scroll a bit, then it decelerates, but you are not actually at the bottom of your scroll view, then it still gets called. I basically want an arrow to show that there is more data in my scroll view, and then disappear when you are at the bottom (like when it bounces). Thanks.

I think what you might be able to do is to check that your contentOffset point is at the bottom of contentSize. So you could probably do something like:
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
float bottomEdge = scrollView.contentOffset.y + scrollView.frame.size.height;
if (bottomEdge >= scrollView.contentSize.height) {
// we are at the end
}
}
You'll likely also need a negative case there to show your indicator when the user scrolls back up. You might also want to add some padding to that so, for example, you could hide the indicator when the user is near the bottom, but not exactly at the bottom.

So if you want it in swift, here you go:
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
if (scrollView.contentOffset.y >= (scrollView.contentSize.height - scrollView.frame.size.height)) {
//reach bottom
}
if (scrollView.contentOffset.y < 0){
//reach top
}
if (scrollView.contentOffset.y >= 0 && scrollView.contentOffset.y < (scrollView.contentSize.height - scrollView.frame.size.height)){
//not top and not bottom
}
}

I Think #bensnider answer is correct, But not exart. Because of these two reasons
1. - (void)scrollViewDidScroll:(UIScrollView *)scrollView{}
This method will call continuously if we check for if (bottomEdge >= scrollView.contentSize.height)
2 . In this if we go for == check also this condition will valid for two times.
(i) when we will scroll up when the end of the scroll view touches the bottom edge
(ii) When the scrollview bounces back to retain it's own position
I feel this is more accurate.
Very few cases this codition is valid for two times also. But User will not come across this.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (scrollView.contentOffset.y == roundf(scrollView.contentSize.height-scrollView.frame.size.height)) {
NSLog(#"we are at the endddd");
//Call your function here...
}
}

The accepted answer works only if the bottom contentInset value is non-negative. A slight evolution would consider the bottom of the contentInset regardless of it's sign:
CGFloat bottomInset = scrollView.contentInset.bottom;
CGFloat bottomEdge = scrollView.contentOffset.y + scrollView.frame.size.height - bottomInset;
if (bottomEdge == scrollView.contentSize.height) {
// Scroll view is scrolled to bottom
}

Actually, rather than just putting #bensnider's code in scrollViewDidScroll, this code (written in Swift 3) would be better performance-wise:
func scrollViewDidEndDragging(_ scrollView: UIScrollView,
willDecelerate decelerate: Bool) {
if !decelerate {
checkHasScrolledToBottom()
}
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
checkHasScrolledToBottom()
}
func checkHasScrolledToBottom() {
let bottomEdge = scrollView.contentOffset.y + scrollView.frame.size.height
if bottomEdge >= scrollView.contentSize.height {
// we are at the end
}
}

It work in Swift 3:
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y == (scrollView.contentSize.height - scrollView.frame.size.height) {
loadMore()
}
}

See what items are currently displayed in the UIView, using something like indexPathsForVisibleRows and if your model has more items than displayed, put an arrow at the bottom.

Related

How can I hide a tableView when I drag it down to a certain point, Swift

I have a textField and when I tap it a tableView appear below. When I scroll the tableView down, say 25% of the height of the tableView I want to hide it. Is it possible ? I am using the scrollViewWillBeginDragging function but its not what I want.
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
let translation = scrollView.panGestureRecognizer.translation(in: scrollView.superview!)
if translation.y > 550 {
self.animateTableView(shouldShow: false)
}
}
Use this UIScrollViewDelegate method:-
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let bottomEdge: CGFloat = scrollView.contentOffset.y + scrollView.frame.size.height
let contentSize = scrollView.contentSize.height * 0.25
if bottomEdge >= contentSize {
/* Code to hide tableView */
}
}

Remove Collection view bounce issue when using pull to refresh

I have one collection view configured as follow
superCollectionView!.alwaysBounceHorizontal = false
superCollectionView!.alwaysBounceVertical = false
if #available(iOS 10.0, *) {
superCollectionView!.refreshControl = refreshControl
} else {
superCollectionView!.backgroundView = refreshControl
}
but bounce effect is still there.
I want to remove bounce from bottom...
If you want to remove bouncing only from the bottom (For letting the refreshControl to be available), I'd suggest to handle it in scroll​View​Did​Scroll:​ method to check if the scroll view contentOffset.y has been reached to bottom of the scroll view (logically, it is the content size of the scroll view minus the height of the visible frame of the scroll view), as follows:
Solution:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y >= scrollView.contentSize.height - scrollView.frame.size.height {
scrollView.setContentOffset(CGPoint(x: scrollView.contentOffset.x, y: scrollView.contentSize.height - scrollView.frame.size.height), animated: false)
}
}
Output:
After implementing scroll​View​Did​Scroll as mentioned above, it should be behaves like:
Also:
What about achieving the opposite?
Referring to the above description, preventing the top bouncing would be:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y < 0 {
scrollView.setContentOffset(CGPoint(x: scrollView.contentOffset.x, y: 0), animated: false)
}
}

Content offset of NSCollectionView

I'm working with NSCollectionView and want to implant paging logic (loading additional content). For that I want to know when user have scrolled to bottom of collection view, because I used to add this approach in iOS apps. How I can do that? Or maybe I need to work with NSScrollView or NSClipView?
Maybe CollectioView have ScrollView delegate method.
Try this:
var lastOffsetY: CGFloat = 0
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y == (scrollView.contentSize.height - scrollView.frame.size.height) {
loadMore()
}
}

UITableView scroll indicator won't hide at top or bottom

I have a problem with UITableView. It won't hide the scroll indicator after:
1) scrolling fast
2) and then hitting the top or bottom of the table.
Here's a screenshot.
How can I make sure the scroll indicator hides correctly as expected?
Please note that bouncing is off. I also don't want to just hide the scroll indicator, I just want it to disappear as expected when scrolling stops at the top or the bottom.
EDIT: This problem seems to be caused by setting the view controller setting automaticallyAdjustsScrollViewInsets to false. It seems that the following 3 things need to be set to reproduce the problem:
1) the table view bounces needs to be off
2) the view controller setting automaticallyAdjustsScrollViewInsets to false (This is to fix a different problem where the scroll indicator does not look right at all)
3) The view of the UIViewController itself should not be the table view, the table view has to be a subview.
In viewDidLoad that will look something like this:
self.view_table = [[UITableView alloc] initWithFrame:self.view.frame];
self.view_table.bounces = false;
self.automaticallyAdjustsScrollViewInsets = false;
Also, the content of the table view needs to be bigger than the height of its frame.
UITableView inherits from UIScrollView, so you'll need to use UIScrollView's properties:
Property: showsVerticalScrollIndicator
A Boolean value that controls whether the vertical scroll indicator is visible.
Take a look at the documentation.
Try this :
-(void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{
if (!scrollView.bounces) {
targetContentOffset->y = -1;//Scrollbar does not move here, because bounces is disabled, but scrollbar can hidden.
}
}
Do the Follwoing steps.
Go to XIB
select the Respective table
Go to Properties and Disable the Horizontal and Vertical Scrollers.
I have an answer based on the answer by 范亚楠.
In the UITableViewDelegate:
Objective-C:
-(void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{
if (!scrollView.bounces) {
if(targetContentOffset->y <= 1)
{
targetContentOffset->y = 0.01;
}
else if(targetContentOffset->y >= scrollView.contentSize.height - scrollView.height)
{
targetContentOffset->y = scrollView.contentSize.height - scrollView.height - 0.01;
}
}
}
Swift 4:
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
guard !scrollView.bounces else {
return
}
if(targetContentOffset.pointee.y <= 1)
{
targetContentOffset.pointee.y = 0.01;
}
else if(targetContentOffset.pointee.y >= scrollView.contentSize.height - scrollView.height)
{
targetContentOffset.pointee.y = scrollView.contentSize.height - scrollView.height - 0.01;
}
}
If the table view is scrolling to the top or the bottom this code makes it stop very close but not quite at the end. This allows the scroll indicator to disappear.
In viewwillLoad()
Make tableView.showsVerticalScrollIndicator = false
to disable the scroll indicators in the scroll action view in the tableView

Making two UIScrollViews follow each others scrolling

How would I make two scroll views follow each others scrolling?
For instance, I have a scroll view (A) on the left of a screen, whose contents can scroll up and down, but not left and right. Scroll view B matches the up and down of A, but can also scroll left and right. Scroll view A is always on the screen.
-----------------------------------------------------------
| | |
| | |
| | |
| A | B |
| | |
| scrolls | |
| up & down | scrolls all directions |
| | |
-----------------------------------------------------------
How would I make it so the the up and down scrolling (of either view) also makes the other view scroll in the same up-down direction? Or is there another method to do this?
Set the delegate of scroll view A to be your view controller... then have...
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGPoint offset = scrollViewB.contentOffset;
offset.y = scrollViewA.contentOffset.y;
[scrollViewB setContentOffset:offset];
}
If you want both to follow each other, then set delegate for both of them and use...
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if([scrollView isEqual:scrollViewA]) {
CGPoint offset = scrollViewB.contentOffset;
offset.y = scrollViewA.contentOffset.y;
[scrollViewB setContentOffset:offset];
} else {
CGPoint offset = scrollViewA.contentOffset;
offset.y = scrollViewB.contentOffset.y;
[scrollViewA setContentOffset:offset];
}
}
The above can be refactored to have a method which takes in two scrollviews and matches one to the other.
- (void)matchScrollView:(UIScrollView *)first toScrollView:(UIScrollView *)second {
CGPoint offset = first.contentOffset;
offset.y = second.contentOffset.y;
[first setContentOffset:offset];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if([scrollView isEqual:scrollViewA]) {
[self matchScrollView:scrollViewB toScrollView:scrollViewA];
} else {
[self matchScrollView:scrollViewA toScrollView:scrollViewB];
}
}
Swift 3 Version:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == scrollViewA {
self.synchronizeScrollView(scrollViewB, toScrollView: scrollViewA)
}
else if scrollView == scrollViewB {
self.synchronizeScrollView(scrollViewA, toScrollView: scrollViewB)
}
}
func synchronizeScrollView(_ scrollViewToScroll: UIScrollView, toScrollView scrolledView: UIScrollView) {
var offset = scrollViewToScroll.contentOffset
offset.y = scrolledView.contentOffset.y
scrollViewToScroll.setContentOffset(offset, animated: false)
}
I tried the Simon Lee's answer on iOS 11. It worked but not very well. The two scroll views was synchronized, but using his method, the scroll views would lost the inertia effect(when it continue to scroll after you release your finger) and the bouncing effect. I think it was due to the fact that setting the contentOffset through setContentOffset(offset, animated: false) method causes cyclic calls of the scrollViewDidScroll(_ scrollView: UIScrollView) delegate's method(see this question)
Here is the solution that worked for me on iOS 11:
// implement UIScrollViewDelegate method
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView == self.scrollViewA {
self.syncScrollView(self.scrollViewB, toScrollView: self.scrollViewA)
}
else if scrollView == self.scrollViewB {
self.syncScrollView(self.scrollViewA, toScrollView: scrollViewB)
}
}
func syncScrollView(_ scrollViewToScroll: UIScrollView, toScrollView scrolledView: UIScrollView) {
var scrollBounds = scrollViewToScroll.bounds
scrollBounds.origin.y = scrolledView.contentOffset.y
scrollViewToScroll.bounds = scrollBounds
}
So instead of setting contentOffset we are using bounds property to sync the other scrollView with the one that was scrolled by the user. This way the delegate method scrollViewDidScroll(_ scrollView: UIScrollView) is not called cyclically and the scrolling happens very smooth and with inertia and bouncing effects as with a single scroll view.
Swift 5.4 // Xcode 13.1
What has worked flawlessly for me was the following:
Create a custom subclass of UIScrollView
Conform to UIGestureRecognizer delegate
Override the gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) GestureRecognizerDelegate method
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
Both collection views need to have the same superview. Add both gesture recognizers to your superview
yourSuperview.addGestureRecognizer(scrollView1.panGestureRecognizer)
yourSuperview.addGestureRecognizer(scrollView2.panGestureRecognizer)
Hope this helps!
Guys i know the question is answered, but decided to share with you here my approach for solving a similar issue that i had, because i believe it is a pretty clean solution. Basically i had to make three collection views scroll together, and what i did, is i made a custom UICollectionView subclass called SharedOffsetCollectionView, that when you set this class to a collection view in storyboards or you instantiate it from code directly, you can have all instances scroll the same.
So with SharedOffsetCollectionView all collection view instances of this class in your app, will scroll the same always. In my opinion it is a clean solution because it requires adding zero logic in your view controllers, it is all contained in this external class, you just have to set the class of your collection view to be SharedOffsetCollectionView and you are done.
The same approach could easily be transferred to UITableViews and UIScrollViews
Hope that is helpful to some you. :)
My solution is written in:
Swift 5.2, XCode 11.4.1
The answer above all did not quite work our for me, since I run into a cyclic call of scrollViewDidScroll. This happend, because setting the content offset of a scroll view also calls scrollViewDidScoll. I solved it by putting a lock between it, which is set based on if a scroll view is being dragged by the user or not, so the syncing won't happen by setting the content offset:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.synchronizeScrollView(offSet: scrollView.contentOffset.y,
scrollViewAIsScrolling: scrollView == scrollViewA,
isScroller: scrollView.isDragging)
}
private enum Scroller {
case page, time
}
private var scroller: Scroller? = nil
func synchronizeScrollView(offSet: CGFloat, scrollViewAIsScrolling: Bool,
isScroller: scrollView.isDragging) {
let scrollViewToScroll = scrollViewAIsScrolling ? scrollViewB : scrollViewA
var offset = scrollViewToScroll.contentOffset
offset.y = offSet
scrollViewToScroll.setContentOffset(offset, animated: false)
}
This code can be refactored based on how many scroll views are used and based on who owns them. I won't recommend having one controller being the delegate of many scroll views. I would rather solve it with delegation.