Handling touches for nested UIScrollViews scrolling in the same direction - ios

I have two nested UIScrollViews, both scrolling in the vertical direction. I need the outer scrollview to scroll to it's max range first before allowing the inner scrollview to scroll. The inner scrollview should not be scrollable until the outer scrollview has reached it's max range. Here's an illustration:
In the left diagram, a vertical drag inside of Scrollview B should move Scrollview A and Scrollview B should not be scrollable (but it still needs to be able to receive touches/taps). Once Scrollview A reaches it's max range (when Scrollview B gets to the top of the screen), then Scrollview B should scroll. This needs to work in one continuous motion.
I've attempted to toggle ScrollView B's scrollEnabled from ScrollView A's scrollViewDidScroll: delegate method, but this doesn't appear to be a viable solution because it doesn't work in one continuous motion (eg: The user needs to release and touch again after Scrollview B reaches the top of the screen).
What's the best way to implement this such that is works in one continuous motion?

I solved the problem in the following way. I am not really happy with it since it looks to me much too complicated, but it works (please note that the code below is a simplified, untested version of my code, which is due to a different UI more complicated):
I have 3 properties that control scrolling:
#property (nonatomic, assign) CGFloat currentPanY;
#property (nonatomic, assign) BOOL scrollA;
#property (nonatomic, assign) BOOL scrollB;
2-step scrolling:
Disable scrolling for B, and enable scrolling for A.
This allows to scroll A .
When A reaches its max position, disable scrolling for A, and enable scrolling for B:
-(void)scrollViewDidScroll: (UIScrollView *)scrollView {
if (scrollView.contentOffset.y >= self.maxScrollUpOffset) {
[scrollView setContentOffset:CGPointMake(0, self.maxScrollUpOffset) animated:NO];
self.scrollviewA.scrollEnabled = NO;
self.scrollviewB.scrollEnabled = YES;
self.scrollB = YES;
}
}
This gives the following effect:
When A is scrolled upwards it will stop scrolling when its max size is reached. However B will not start scrolling, since the pan gesture recognizer of A does not forward its actions to the pan gesture recognizer of B. Thus one has to lift the finger and to start a 2nd scrolling. Then, B will scroll. This gives the 2-step scrolling.
Continuous scrolling:
For continuous scrolling, B must scroll while the finger that started scrolling of A continues moving upwards. To detect this, I added a further pan gesture recognizer tho A, and enabled it to detect gestures simultaneously with the built-in gesture recognizers of A and B:
- (BOOL)gestureRecognizer:(UIPanGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UISwipeGestureRecognizer *)otherGestureRecognizer {
return YES;
}
In the action of this additional pan gesture recognizer, I compute the distance the finger has moved upwards after the scrolling limit of A has been reached. By this distance, B is then scrolled programmatically:
- (void)panGestureRecognizerAction:(UIPanGestureRecognizer *)recognizer {
if (recognizer.state != UIGestureRecognizerStateChanged) {
self.currentPanY = 0;
self.scrollB = NO;
self.scrollA = NO;
} else {
CGPoint currentTranslation = [recognizer translationInView:self.scrollviewA];
CGFloat currentYup = currentTranslation.y;
if (self.scrollA || self.scrollB) {
if (self.currentPanY == 0) {
self.currentPanY = currentYup;
}
CGFloat additionalYup = self.currentPanY - currentYup;
if (self.scrollA) {
CGFloat offset = self.scrollviewA.scrollUpOffset + additionalYup;
if (offset >= 0) {
self.scrollviewA.contentOffset = CGPointMake(0, offset);
} else {
self.scrollviewA.contentOffset = CGPointZero;
}
} else if (self.scrollB){
self.scrollviewB.contentOffset = CGPointMake(0, additionalYup);
}
}
}
}
There is still a disadvantage:
If you start scrolling, lift the finger, and let the scrollView decelerate, it will behave like the 2-stage scrolling, since the additional pan gesture recognizer won’t recognize any pan gesture.

In my case I solved subclassing UIScrollView for the outer ScrollView.
class MYOuterScrollView: UIScrollView, UIGestureRecognizerDelegate
{
override func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool
{
return true
}
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool
{
return true
}
}

Your requirement that this should work in one continuous motion calls out the answer: you need to use just one UIScrollView, not two.
If you have just one scrollview you can then perform your magic by overriding the scrollview's layoutSubviews method and rejigging its content to perform your parallax effect based on the current value of contentOffset. Make sure that contentSize always reflect the full height (you can even update contentSize inside layoutSubviews if needs be).
For an architecture, take your existing diagram and just replace Scrollview B with View B.

The gesture recognizer for scroll view A would need to pass off to the gesture recognizer for scroll view B to have on continuous motion which I am pretty sure is not possible. Why not combine the content of the two scroll views instead and then you would have one continuous motion. This code will combine the content of scrollView A and B into just A.
UIScrollView* scrollViewA = ...;
UIScrollView* scrollViewB = ...;
NSArray* subviews = scrollViewB.subviews;
for (int i = 0; i < subviews.count; i++)
{
UIView* subview = [subviews objectAtIndex:i];
CGRect frame = subview.frame;
frame.origin.y += scrollViewA.contentSize.height;
subview.frame = frame;
[scrollViewA addSubview:subview];
}
CGSize size = scrollViewA.contentSize;
size.height += scrollViewB.contentSize.height;
scrollViewA.contentSize = size;

Same-Direction Scrolling
Same direction scrolling occurs when a UIScrollView that is a subview of a UIScrollView both scroll in the same direction. This is shown in the left image.
Follow this link if want to look in details https://developer.apple.com/library/ios/documentation/WindowsViews/Conceptual/UIScrollView_pg/NestedScrollViews/NestedScrollViews.html
Just set some attributes which are mentioned in above image. it works.

Related

using scrollViewWillEndDragging to ensure UIScrollView stops at defined increments

I would like my UIScrollView to naturally glide to endings at certain incremental values, corresponding to every 50 points of width of a horizontally-scrolling UIScrollView To do this, I customed scrollViewWillEndDragging, like so (as recommended, but not described in detail in an answer here Scrolling a horizontal UIScrollView in stepped increments?):
- (void) scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{
if(fmodf(targetContentOffset->x, 50.0)){
int roundingNumber1 = 50;
CGFloat newOffset = roundingNumber1 * floor(((scrollView.contentOffset.x)/roundingNumber1)+0.5);
targetContentOffset->x = newOffset;
}
}
However, I am not sure this is actually stopping the view at specific increments, and I also notice that the effect is asymmetric. Though my scrolling motions/velocity/etc are the same, scrolling right is much less fluid than scrolling left. Scrolling right stops faster and more abruptly. There's a video here. Why is this behavior asymmetric and how can I change it?
The reason I thin the scrolling is not stopping at increments of 50 is that I also have a UILabel underneath the scroll view that gets updated by other delegate functions to show the offset. The value it shows is rarely close to 50 when the scrolling is done. Is this because the scrolling is not incrementing to values of 50 or because I am not updating at the right times?
Thanks for any advice.
- (void) scrollViewDidScroll:(UIScrollView *)scrollView{
if(abs(self.lastOffset - scrollView.contentOffset.x) > 49){
CGFloat newNumber = scrollView.contentOffset.x;
self.numberProperty.text = [NSString stringWithFormat:#"%.00f", scrollView.contentOffset.x];
self.lastOffset = scrollView.contentOffset.x;
[self.view setNeedsDisplay];
}
}
- (void) scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView{
self.numberProperty.text = [NSString stringWithFormat:#"%.00f", scrollView.contentOffset.x];
}
Rather than implementing these delegate methods, you can set pagingEnabled on your scroll view to true to get this behavior for free. When paging is enabled, the scroll view will automatically snap to the nearest "page" when you end dragging, where the page width is equal to the scroll view's width.
Based on your video, it looks like the items in your scroll view are smaller than the width of the scroll view itself. To use paging, you'll have to do the following:
Make your scroll view have the same width as one of your items (50.0 units in your case).
Set scrollView.clipsToBounds to false so that the scroll view draws subviews outside of its much smaller bounds.
Set scrollView.pagingEnabled to true so that the scroll view scrolls with paging.
At this point, paging will work but you won't be able to drag the scroll view outside of its bounds. To make this work, you'll need to embed the scroll view in a larger view that forwards touch events to it.
Create a "touch forwarding" class and add it to your view.
This class takes all touch events it receives and sends them to its targetView property instead. DJK is a random prefix I made up for the class name.
#interface DJKTouchForwardingView : UIView
/** The view to which touch events should be forwarded. */
#property (weak, nonatomic) UIView *targetView;
#end
#implementation DJKTouchForwardingView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *child = nil;
if ((child = [super hitTest:point withEvent:event]) == self) {
return self.targetView;
}
return child;
}
#end
Make the scroll view a subview of the touch forwarding view and assign it to the targetView property.
Your scroll view will now be scrollable within the bounds of the touch forwarding view and will snap to 50 unit pages.

How to animate views according to tableview scrolling directions without content scrolling

i'm trying to implement a specific tableview behaviour (Like on Facebook app).
I want to have a dynamic header that will be magnified every time the user scrolls up and will be shrieked when the user scroll down.
In addition i want the tableview to cause the effect of pushing the header and than scrolling the tableview cells.
I used the method:
- (void)scrollViewDidScroll:(UIScrollView *)aScrollView
in this method i calculated the offset and the direction and called a method that shrink or magnify the header accordingly
so far so good.
The thing is that the animation is being performed with the tableview scrolling.
To avoid it, I created a custom scrollview on to of the top of my tableview, I taged the two scrollviews differently.
In the scrollview i created a weak reference of the tableview and a boolean value that indicated if the scrollview should return the tableview touch.
When the shrinking\magnifying animation was finished i changed the boolean value so it will signal the custom scrollview to return the tableview in my HitTest methods that i implemented inside the scrollview.
But hitTest not called when the user keep scrolling (without leafing the finger), in additions now my buttons inside my tableViewCell aren't reacting.
Here is my HitTest Method:
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView* result = [super hitTest:point withEvent:event];
if (_recieveTouchOnTable)
{
return _table;
}
else
return result;
}
Here is my scrollViewDidScroll method:
(onProgress means that the animation is being performed, so keep returning the custom scrollview)
Tag = 2 = the custom scrollview
Tag = 1 = the tableview
- (void)scrollViewDidScroll:(UIScrollView *)aScrollView {
CGFloat yVelocity = [aScrollView.panGestureRecognizer velocityInView:aScrollView].y;
CGFloat offset = lastCustomScrollViewContentOffset.y-aScrollView.contentOffset.y;
lastCustomScrollViewContentOffset =aScrollView.contentOffset;
if (yVelocity<0)
offset = fabs(offset)*-1;
else if(yVelocity>0)
offset = fabs(offset);
if (offset!=0 && aScrollView.tag == 2)
[self layoutViewAccorrdingToTableviewScorlingVelocity:offset];
if (!onProgress ){
customScrollView.recieveTouchOnTable=YES;
}
}
Am i missing something, or maybe there's a more simple way to do it?

Handling Conflicting Gestures in Nested UIScrollViews

So I realize that having nested scroll views at all it sort of a red flag, but the current setup of everything actually works quite well besides one small problem. One scroll view manages scrolling through a collection, while another handles zooming and panning on the entire collection view. This all works, but the small problem comes from when zooming in and panning downward, the scrollview pans while the collectionview scrolls, causing the view to scroll twice as fast, and not feel connected to your finger.
What I ideally want to happen is vertical scrolling is managed by the outer scroll view when panning is possible, and then handled by the inner scroll view when the outer one can no longer pan. I got very close by writing something like this:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView == _outerScrollView) {
CGPoint offset = _outerScrollView.contentOffset;
CGFloat height = _outerScrollView.frame.size.height;
CGFloat contentHeight = _outerScrollView.contentSize.height;
ScrollDirection scrollDirection;
if (offset > _lastContentOffset){
scrollDirection = ScrollDirectionUp;
} else {
scrollDirection = ScrollDirectionDown;
}
BOOL scrollIsAtTop = offset.y <= 0;
BOOL scrollIsAtBottom = offset.y + height >= contentHeight;
//If there is a pan upward and we aren't at the top of the outer
//scrollview cancel the gesture on the inner view
//downward vice versa
if (!((scrollIsAtTop && scrollDirection == ScrollDirectionUp)
|| (scrollIsAtBottom && scrollDirection == ScrollDirectionDown))) {
_innerCollectionView.panGestureRecognizer.enabled = NO;
_innerCollectionView.panGestureRecognizer.enabled = YES;
}
}
_lastContentOffset = offset.y;
}
This ALMOST works, with one side effect of a big pan downward stops when it hits the bottom and requires the user to start a new gesture to continue scrolling with the inner collection. Ideally this transition would be smooth, but I'm having a hard time figuring out a way to do this. Again I realize scroll view inside scroll view is not ideal, but if I can fix this small problem everything will be good, rather than attempt to redesign the whole thing.
Any ideas on how I can handle the double scroll in a way that lets the pan gesture win, but cleanly transitions to the inner collection when the outer scroll view can no longer pan vertically?
So, since I never got any answers, this is the solution I've been going with. Essentially if the inner collection view isn't at the top or bottom, I reset the y offset change the outer scroll view has in scrollViewDidScroll. Code looks like this:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView == _outerScrollView) {
if (![self innerScrollIsAtTop] && ![self innerScrollIsAtBottom] && !self.allowOuterScroll) {
[scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, self.lastContentOffset)];
}
self.lastContentOffset = scrollView.contentOffset.y;
}
}
using these 2 conveniences:
- (BOOL)innerScrollIsAtTop {
return _innerCollectionView.contentOffset.y <= 0;
}
- (BOOL)innerScrollIsAtBottom {
CGFloat zoom = self.zoomScale;
CGFloat height = _innerCollectionView.frame.size.height;
CGFloat contentHeight = _innerCollectionView.contentSize.height;
CGPoint offset = _innerCollectionView.contentOffset;
return offset.y + height / zoom >= contentHeight;
}
And you'll need 2 class variables, a float to hold the previous y content offset of the outer scroll, and a BOOL to hold whether you want to allow the outer scroll view to scroll, which you can set to YES while zooming or programatically scrolling. This solution fixes the double scroll, but does have a cumbersome hack within scrollviewDidScroll that may bite you later and you constantly need to work around, but for now this is the solution I've been using.

How to trick UIScrollView into starting scrolling in the middle of panning?

Example:
You have a UIScrollView with scrollEnabled = NO.
After user pans a certain distance, you want the scroll view to start scrolling.
When finger goes down on screen and I disable scrolling, the scroll view is dead until the finger is lifted. Enabling scrolling in middle of a pan doesn't cause it to start.
I suspect it is because UIScrollView will decide to ignore the touch sequence if it was scrollEnabled=NO during its internal touchesBegan:withEvent: (or its private pan gesture recognizer).
How would you trick it into starting to scroll natively (with bounce effect) in the middle of a touch sequence?
Reason: For a simple UIKit based game that requires this mechanic.
Okay, this still needs a little bit of tweaking to handle scrolling again after the first scroll has stopped, but I think the key to what you're trying to achieve is to implement the UIScrollViewDelegate method scrollViewDidScroll:. From here, you can save the starting content offset of the scroll view, and compare the scroll view's current offset to the starting offset added to the y translation of the scroll view's pan gesture recognizer.
The code below successfully doesn't allow scrolling to start until the user has panned at least 100 pts. Like I said, this needs a little tweaking, but should be enough to get you going.
- (void)viewDidLoad
{
[super viewDidLoad];
[self setStartingOffset:CGPointMake(0.0, -1.0)];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (self.startingOffset.y < 0.0) {
[self setStartingOffset:scrollView.contentOffset];
}else{
static CGFloat min = 100.0;
CGFloat yTranslation = fabs([scrollView.panGestureRecognizer translationInView:scrollView].y);
if (yTranslation < self.startingOffset.y + min) {
[scrollView setContentOffset:self.startingOffset];
}else{
if (self.startingOffset.y >= 0.0) {
[self setStartingOffset:CGPointMake(0.0, -1.0)];
}
}
}
}

How to disable horizontal scrolling in UIScrollView?

I have a scrollView of 280(w) x 350(h) and a content view of 840(w) x 405(h).
I switch between the views with a segmented control like this:
- (IBAction)segmentedClicked:(UISegmentedControl *)sender
{
CGFloat x = sender.selectedSegmentIndex * self.personalDetailsScrollView.frame.size.width;
CGPoint point = CGPointMake(x, 0);
[self.personalDetailsScrollView setContentOffset:point animated:YES];
}
I want to disable the regular horizontal scrolling so only the segmented buttons will scroll the view horizontally. The vertical scroll should stay active.
Tried to use -(void)scrollViewDidScroll and the solution offered here: (How to lock the horizontal scrolling of a scrollView in iOS) but it didn't work for me for some reason.
I haven't tried it, but this is the approach I'd take:
In UIScrollView, there is a property:
#property(nonatomic, readonly) UIPanGestureRecognizer *panGestureRecognizer
So I'd make my own new UIPanGestureRecognizer extension and override:
- (CGPoint)translationInView:(UIView *)view
{
CGPoint tmp = [super translationInView:view];
return CGPointMake(0, tmp.y);
}
Add that gesture recognizer to the scrollview, then call:
[scrollView.panGestureRecognizer requireGestureRecognizerToFail:youCustomOne];
It should work.
try this
- (IBAction)segmentedClicked:(UISegmentedControl *)sender
{
if(sender.selectedSegmentIndex == 0)
{
CGPoint point = CGPointMake(0, 0);
}
else
{
CGPoint point = CGPointMake(self.personalDetailsScrollView.frame.size.width, 0);
}
[self.personalDetailsScrollView setContentOffset:point animated:YES];
}
I know this is a late answer, but I see there is still no accepted answer, so I'll give it a go.
It sounds like what you are trying to do is use 1 controller to effectively hold 3 separate screens. You're trying to switch between the screens depending on what the user selects in a UISegmentedControl, and you don't want the user to be able to pan between the views by just horizontally swiping. Why not just have 3 different view controllers? I think that is the more expected way to setup what you are trying to do, and therefore you will find it much easier to implement.
Swift 3, Set UIScrollViewDelegate to your class and then use below code.
scrollView.isDirectionalLockEnabled = true
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.x>0 {
webView.scrollView.contentOffset.x = 0.0
}
}

Resources