I have UIScrollView (inner - UICollectionView to be honest) in VC which is in paged UIScrollView (outer). Both are horizontal.
When I scroll inner to edge (either left or right side) it starts passing touches to outer scroll and outer scroll starts scrolling.
How to avoid that kind of behaviour? I want outer scroll to not react on touches in inner scroll.
You can put an UIView under the outer UIScrollView, and set it to userInteractionEnabled to NO
I found out how to do this:
Subclass UIScrollView - this will be your outer scrollView
now the method:
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
UIView *touchedView = gestureRecognizer.view;
CGPoint loc = [gestureRecognizer locationInView:touchedView];
UIView* subview = [touchedView hitTest:loc withEvent:nil];
if([[subview nextResponder] isKindOfClass:[MyInnerScrollVC class]]) {
return NO;
} else {
return YES;
}
}
and thats it
Related
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.
I'am facing a little problem with my App that has a bit complicated structure :
So first things first the views hirearchy :
|- UICollectionView (horizontal scrolling)
-> UICollectionViewCell
|-> UIScrollView A (PagingEnabled = YES, vertical scrolling only)
|-> UIImageView = page 1
|-> UIScrollView B (PagingEnabled = NO, vertical scrolling only) = page 2
|-> UIView (content...)
The problem is that when I scroll on UIScrollView A to make page 2 appear, the first scroll to the see the bottom on UIScrollView B is ignored, no scroll at all, but the second one has the right behaviour.
I think that it's a kind of "focusing" problem, but I can't figure out how to make this work properly.
Any clue on how to give this first touch on page 2 to the right UIScrollView ?
Have you tried hitTest:withEvent: method? This method helps you choose which view should receive the touch.
For example, you could subclass UIScrollView and set your UIScrollView A's class the new class you just created. Adding the code below to this custom UIScrollView class will let you deliver the touch to it's child view's first
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
for (UIView *subView in self.subviews) {
// Convert the point to the subView's coordinate system.
CGPoint pointForTargetView = [subView convertPoint:point fromView:self];
if (CGRectContainsPoint(subView.bounds, pointForTargetView)) {
// Calling subView's hitTest:withEvent get the right view
return [subView hitTest:pointForTargetView withEvent:event];
}
}
UIView *hitView = [super hitTest:point withEvent:event];
// // Uncomment if you want to make the view 'transparent' to touches:
// // meaning the parts of the view without any subview will deliver the touch
// // to the views 'behind' it
// if (hitView == self) {
// return nil;
// }
return hitView;
}
For more detailed info please check WWDC - Advanced Scrollviews and Touch Handling Techniques (around 15:00 is where this subject starts).
Also, thanks to Tim Arnold for the clue.
I have a UITableview with a subclass of UIVIew on top of a section of the table, like so:
-uitable--
|--uiview-|
|| ||
|| ||
|---------|
| |
| |
-----------
The view has the same superview as the tableview, but covers the tableview partially. There are a few buttons on the UIView. I want the user to be able to scroll on the view and subsequently move the tableview (as if he were scrolling on the table). But, if he taps a button on the view, I want that tap to register on the view, not get sent down to the tableview. Right now, I am overriding the view's - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event method to always return NO, which works for the scrolling but has the effect of sending all touches down to the tableview, rendering my buttons ineffective. Is there any way for me to pass down swipe/pan gestures to the tableview but keep the tap gestures?
Thanks,
Instead of always returning NO from pointInside, check to see if the point intersects any of your UIButton subviews - and return YES if it does.
- (BOOL) pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
for ( UIView* sv in self.subviews )
{
if ( CGRectContainsPoint( sv.frame, point) )
return YES;
}
return NO;
}
EDIT: alternate solution
Per your comment you'd like the user to begin scrolling the UITableView with a touch-down on one of the subview buttons. To make this work you'll need to make the UIView that contains your buttons a subview of the UITableView.
By doing this the UIView will then begin to scroll along with the UITableViewCells. To prevent this you need to adjust the frame as scrolling happens, or possibly lock it in place using constraints.
- (void) viewDidLoad
{
[super viewDidLoad];
// instantiate your subview containing buttons. mine is coming from a nib.
UINib* n = [UINib nibWithNibName: #"TSView" bundle: nil];
NSArray* objs = [n instantiateWithOwner: nil options: nil];
_tsv = objs.firstObject;
// add it to the tableview:
[self.tableView addSubview: _tsv];
}
// this is a UIScrollView delegate method - but UITableView IS a UIScrollView...
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
[scrollView bringSubviewToFront:_tsv];
CGRect fixedFrame = _tsv.frame;
fixedFrame.origin.y = 100 + scrollView.contentOffset.y;
_tsv.frame = fixedFrame;
}
I'm trying to figure out how much of the scroll view inside my UIPageViewController has been scrolled. I have the following code in the viewLoad that access the UIScrollView from UIPageViewController.
for (UIView *view in self.pageViewController.view.subviews)
{
if ([view isKindOfClass:[UIScrollView class]])
{
thisControl = (UIScrollView *)view;
}
}
So once I have the UIScrollView, called thisControl I would like to print how much has been scrolled by doing something like this:
NSLog (#"Scroll: %f",thisControl.contentOffset.x);
The problem, is that I don't really know where to put this line, or how print continuously how much has been scrolled.
Any idea?
Set the delegate to that controller(myScroll.delegate = self;) and then add this method:
- (void) scrollViewDidScroll: (UIScrollView *) scrollView
{
CGFloat y = scrollView.contentOffset.y;
NSLog(#"%f",y);
// some stuff
}
I have a overlay UIView above a UITableview. When I scroll the UITableview, I want the overlay UIView to move at the same speed as UITableview scroll,just as the top menu of the facebook app.
Any suggestions?
I try to change the overlay uiview's frame in uitableview's scroll delegate.something like:
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
self.lastOffsetY = scrollView.contentOffset.y;
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGFloat offset = self.lastOffsetOfY - scrollView.contentOffset.y;
CGRect frame = self.overlayview.frame;
frame.origin.y += offset;
self.overlayview.frame = frame;
self.lastOffsetOfY = scrollView.contentOffset.y;
}
just use change of contentoffset value. but the overlay uiview doesn't move the same as the uitableview scroll.
If you just want that UIView to stick to the top of the screen at all times, independent of the tableview you just need to add the UIView to your controller's view instead of adding it to the tableview.
In your XIB, if you're using one, You should have something like
VIEW
UITableView
UIView
Instead of what you probably have:
VIEW
UITableView
UIView
If you're trying to accomplish an overlay similar to the Facebook app, it's going to be more efficient to add the overlay to the superview that houses your UITableView/ScollView. This way you won't need to write a bunch of code that needs to execute every time your user scrolls.
//in your View Controller:
UITableView *table = [[[UITableview alloc] init] autorelease];
CGRect frame = CGRectMake (tableOriginX, tableOriginY, width, height);
UIView *overlay = [[[UIView alloc] initWithDesieredFrame: frame] autorelease];
This way the view never moves, but gives the illusion of always existing at the top of the TableView. If, like facebook, you wish to dismiss the view, you can use the TableView's internal gesture recognizers to detect a flick (like facebook) that will dismiss or summon the overlay.
//Something like:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
if (velocity.y > 0)
{
[self hideButtonPannelData];
}
if (velocity.y < -0.8)
{
[self revealButtonPannelData];
}