Limiting the scrollable area in UIScrollView - iphone

I have a UIScrollView that is scrolling a fairly large UIView.
At certain times I want to limit the area the user can scroll around in. For example, I may only want to allow them to view the bottom quarter of the view.
I am able to limit the area by overriding scrollViewDidScroll and then calling setContentOffset if the view has scrolled too far. But this way I can't get it bounce back as smoothly as the UIScrollView can naturally do when scrolling beyond the bounds of the UIView.
Is there a better way to limit the scrollable area in a UIScrollView?

I would change the contentSize property of the scroll view to the size of the area you want the user to be able to scroll around in and adjust the frame.origin of the subview such the upper left boundary you want appears at (0, 0) relative to the scroll view. For example, if your view is 800 points tall and you want to show the bottom quarter, set the height of contentSize to 200 and set the y component of view.frame.origin to -600.

I've found something that works for me. It let's you scroll to point 0,0 but no further:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (scrollView.contentOffset.x <= -1) {
[scrollView setScrollEnabled:NO];
[self.scrollView setContentOffset:CGPointMake(0, 0) animated:YES];
[scrollView setScrollEnabled:YES];
}
}
You could do the same for top, bottom or right (x or y)

a small improvement on Yoko's answer in Swift 4 will be
override func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y > 600 {
let anim = UIViewPropertyAnimator(duration: 1, dampingRatio: 0.5) {
scrollView.isScrollEnabled = false
scrollView.setContentOffset(CGPoint(x: 0, y: 600), animated: false)
scrollView.isScrollEnabled = true
}
anim.startAnimation()
}
}
which will make the scrollview animate really similar to what its supposed to do. The slower drag when you are in the "bounce" area will not work and animation duration has to depend on the distance (not constant like here) if you want to be exact. You can also try to do this logic in scrollViewDidScroll and see how it differs. The key thing is that setContentOffset(_:,animated:) has to be with animated: false so that the UIViewPropertyAnimator's block can capture it and animate it

Another approach is to override the UIScrollView's method:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event.
Returning YES will allow the user to scroll. Returning NO will not.
NOTE: This will disable all touches to any views imbedded inside the UIScrollView that pointInside returns NO to. Useful if the area you don't want to scroll from doesn't have any interaction.
This example only allows the UIScrollView to scroll when the user is scrolling over a UITableView. (A UITableView and two UIViews are imbedded inside the UIScrollView)
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
for (UIView *subview in self.subviews) {
if ([subview pointInside:[self convertPoint:point toView:subview] withEvent:event] && ![subview isKindOfClass:[UITableView class]]) {
return NO;
}
}
return YES;
}

Related

Objective C: How can use two scrollview, one inside the another?

I have two scrollview, one scrollview inside another and I want to scroll my main scrollview content first after that only I want to scroll subview scroll
CGFloat scrollOffset = texscrl.contentOffset.y;
if (scrollOffset == 0)
{
//This condition will be true when scrollview will reach to bottom
self.ArtistScroll.scrollEnabled=YES;
texscrl.scrollEnabled=YES;
}else
{
self.ArtistScroll.scrollEnabled=NO;
texscrl.scrollEnabled=YES;
}
Here, am using content offset for this.. so can anyone help me?
You can achieve the affect you describe by using a third scrollview to actually handle the touch gestures and manually set the contentOffset of the other scrollviews.
Here is how to achieve this for vertically scrolling content, which is I think what you are describing. In the code, outerScrollView is the main scrollview, innerScrollView is the sub-scrollview that is contained by the outer scrollview, and trackingScrollView is the third scrollview that only handles the touches - it has no content.
Create the three scrollviews such that trackingScrollView exactly covers outerScrollView. (I assume you will do this in XIB or storyboard so there is no code.)
Once your scrollviews have content (and whenever their contentSize or bounds change) you should set the contentSize of trackingScrollView:
self.trackingScrollView.contentSize = CGSizeMake(self.trackingScrollView.bounds.size.width,
self.outerScrollView.contentSize.height + self.innerScrollView.contentSize.height - self.innerScrollView.bounds.height);
This makes the content height be that of outerScrollView plus the scrollable distance of innerScrollView, allowing trackingScrollView to scroll over the total travel distance of both scrollviews.
Delegate for trackingScrollView and implement scrollViewDidScroll:
- (void) scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView == self.trackingScrollView) {
CGFloat const offsetY = self.trackingScrollView.contentOffset.y;
// Calculate the maximum outer scroll offset
CGFloat const maxOuterOffsetY = self.outerScrollView.contentSize.height - self.outerScrollView.bounds.height;
// Calculate the maximum inner scroll offset
CGFloat const maxInnerOffsetY = self.innerScrollView.contentSize.height - self.innerScrollView.bounds.height;
if (offsetY < maxOuterOffsetY) {
// Scroll is within the outer scroll area or the top bounce zone
self.outerScrollView.contentOffset = CGPointMake(0, offsetY);
self.innerScrollView.contentOffset = CGPointZero;
} else if (offsetY < maxOuterOffsetY + maxInnerOffsetY) {
// Scroll is within the inner scroll area
self.outerScrollView.contentOffset = CGPointMake(0, maxOuterOffsetY);
self.innerScrollView.contentOffset = CGPointMake(0, offsetY - maxOuterOffsetY);
} else {
// Scroll is within the bottom bounce zone
self.outerScrollView.contentOffset = CGPointMake(0, offsetY - maxInnerOffsetY);
self.innerScrollView.contentOffset = CGPointMake(0, maxInnerOffsetY);
}
} else {
// Handle other scrollviews as required
}
}
Effectively we divide the tracking scroll area into two parts. The top part controls the scrolling within the outer scrollview and the bottom part controls the inner scrollview.
Assuming bounce is turned on (which generally it should be) we also need to handle scrolling outside the scroll area. We want the bounce to show on the outer scrollview so the top bounce is handled implicitly by the first conditional. However, the bottom bounce has to be handled explicitly by the third conditional, otherwise we would see the bounce on the inner scrollview.
To be clear, in this solution the two content scrollviews (outerScrollView and innerScrollView) do not receive any touches at all; all input is going to trackingScrollView. If you want input to subviews of the content scrollviews you will need a more advanced solution. I believe this can be done by putting trackingScrollView behind outerScrollView, removing outerScrollView's gesture recognizers and replacing them with those from trackingScrollView. I have seen this technique presented in an old WWDC session on UIScrollView but I have not tried it myself.

How to control two UIScrollViews in the same time with one touch?

Anyone has an idea how to control two scroll views while one in on the top of the other. One scrolls, the other one scrolls too. Same with zoom, gesture recognisers, etc ... Kinda like passing replica of the touches received by the first view onto the the one underneath. Subclass of the top scroll view has got a weak reference to the "dependant" scroll view underneath. Very important is to get the delegate methods working for both scrollviews as there is a lot of logic in these ...
use the scrollView Delegate methood
- (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];
}
}
or simply in the same method for both horizontal an vertical scrolling
if([scrollView isEqual:scrollViewA]) {
scrollViewB.contentOffset = scrollViewA.contentOffset;
}
and viceVersa

Resize UICollectionView while scrolling

My collection view begins life short, at the bottom of it's view controller's view. When the user scrolls up a little bit, I'd like the collection to fill the view. I'd like it to shrink again when the user pulls down into the bounce area.
Here's the code that I'd like to make work (and I think should work):
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView == self.collectionView) {
CGFloat offsetY = self.collectionView.contentOffset.y;
if (offsetY > 20.0) {
[self setCollectionViewExpanded:YES];
} else if (offsetY < -10.0) {
[self setCollectionViewExpanded:NO];
}
}
}
- (void)setCollectionViewExpanded:(BOOL)expanded {
// avoid starting a do-nothing animation
BOOL currentlyExpanded = self.collectionView.frame.origin.y == 0.0;
if (expanded == currentlyExpanded) return;
// note: the collection view is the uppermost subview of the vc's view
// expanding to view bounds makes it cover every other view
CGRect newFrame = (expanded)? self.view.bounds : CGRectMake(0.0, 240.0, 320.0, self.view.bounds.size.height-240.0);
[UIView animateWithDuration:0.3 animations:^{
self.collectionView.frame = newFrame;
}];
}
But after the expand happens, the cells remain in the same position relative to the parent view (still near the bottom) and the collection view stops responding to panning touches and sends no further didScroll notification.
I can get the content repositioned properly by doing a reloadData in an animation completion block, but the scrolling touches stop working either way.
A while back, I tried something like this with a table view (or another kind of scroll view) and ran into a similar hangup. Much obliged if anyone has this solved...
Can you describe your view hierarchy a bit better? Is the UICollectionView a subview of the UIScrollView?
Is self.view a UIScrollView?
In that case, the bounds origin is the scrollview's contentOffset (not 0,0). That could give your UICollectionView an unexpected frame.
If the UICollectionView is a subview of the scrollview, the frame origin is the origin of the view in the scrollview-space. So don't change the frame's origin (just its size) and set the contentOffset of the scrollView to make it visible.

UITextView - disable vertical scrolling

How can I disable vertical scrolling in my UITextView? I want it to basically just scroll horizontally.
In some circumstances, when trying to clamp down on unwanted UITextView scrolling I have found it helpful to add something like the following to the UITextView delegate (this is a UIScrollView delegate method but, of course, UITextView inherits from UIScrollView). This might work for you.
- (void)scrollViewDidScroll:(id)scrollView
{
CGPoint origin = [scrollView contentOffset];
[scrollView setContentOffset:CGPointMake(origin.x, 0.0)];
}
What about the scrollEnabled property? Setting the scrollEnabled property to NO stops the user from scrolling (in both directions), but there are occasions where the system sends setContentOffset:animated: messages to a UITextView. The scrollEnabled property applies to both vertical and horizontal scrolling. Given your question, you might want to leave scrollEnabled as is.
You can change it from Xcode -
Solution for disabling vertical scrolling for Swift 4:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let origin: CGPoint = scrollView.contentOffset
scrollView.contentOffset = CGPoint(x: origin.x, y: 0.0)
}
If you have your custom textView subclass, you can override -gestureRecognizerShouldBegin to disable the scroll.
-(BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]])
{
if (gestureRecognizer.view == self)
{
return NO;
}
else
{
return [super gestureRecognizerShouldBegin: gestureRecognizer];
}
}
}
why not just use a UITextField if you dont want vertical scrolling?
Just set the contentSize to the height of the view.
You'll use this:
CGSize scrollableSize = CGSizeMake(widthOfContent, heightOfView);
[myScrollView setContentSize:scrollableSize];
place your UITextView in a UIScrollView.
Set your UITextView.frame to a Size the complete Text fits in a Line and set the contenSize of the ScrollView to the size of your UITextView.frame.
Cheers
nettz

UIScrollView: single tap scrolls it to top

I have the UIScrollView with pagingEnabled set to YES, and programmatically scroll its content to bottom:
CGPoint contentOffset = scrollView.contentOffset;
contentOffset.y = scrollView.contentSize.height - scrollView.frame.size.height;
[scrollView setContentOffset:contentOffset animated:YES];
it scrolls successfully, but after that, on single tap its content scrolls up to offset that it has just before it scrolls down. That happens only when I programmaticaly scroll scrollView's content to bottom and then tap. When I scroll to any other offset and then tap, nothing is happened.
That's definitely not what I'd like to get. How that should be fixed?
Much thanks in advance!
Timur.
This small hack prevents the UIScrollView from scrolling when tapped. Looks like this is happening when the scroll view has paging enabled.
In your UIScrollView delegate add this method:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
scrollView.pagingEnabled = self.scrollView.contentOffset.x < (self.scrollView.contentSize.width - self.scrollView.frame.size.width);
}
This disables the paging when the scroll view reaches the right end in horizontal scrolling (my use case, you can adapt it to other directions easily).
I just figured out what causes this problem, and how to avoid it. If you having pagingEnabled set to YES on a scroll view, you must set the contentOffset to be a multiple of the scroll view's visible size (i.e. you should be on a paging boundary).
Concrete example:
If your scroll view was (say) 460 pixels high with a content area of 920, you would need to set the content offset to EITHER 0 or 460 if you want to avoid the "scroll to beginning on tap" problem.
As a bonus, the end result will probably look better since your scroll view will be aligned with the paging boundaries. :)
The following workaround did help (assume that one extends UIScrollView with a category, so 'self' refers to its instance):
-(BOOL) scrolledToBottom
{
return (self.contentSize.height <= self.frame.size.height) ||
(self.contentOffset.y == self.contentSize.height - self.frame.size.height);
}
Then, one should sometimes turn pagingEnabled off, just at the position where scroll view reaches its bottom. In the delegate (pagingEnabled is initialy on of course, since the problem occurs only when it is enabled):
-(void) scrollViewDidScroll:(UIScrollView *)scrollView
{
if (scrollView.pagingEnabled == YES)
{
if ([scrollView scrolledToBottom] == YES)
scrollView.pagingEnabled = NO;
}
else
{
if ([scrollView scrolledToBottom] == NO)
scrollView.pagingEnabled = YES;
}
}
This seems to be a bug:
UIScrollView doesn't remember the position
I have tested this on iOS 4.2 (Simulator) and the issue remains.
When scrolling a ScrollView I would suggest using
[scrollView scrollRectToVisible:CGRectMake(0,0,1,1) animated:YES];
Where the rect is the position you're after. (In this case the rect would be the top of the scrollview).
Changing the content offset is not the correct way of scrolling a scrollview.