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

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)];
}
}
}
}

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.

Handling touches for nested UIScrollViews scrolling in the same direction

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.

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.

UICollectionView that only scrolls right (no left, up or down)

How would you go about implementing a UICollectionView with this behaviour?
The idea is that once a user navigates past a certain point, they cannot go back and view those cells again.
My attempt at a solution has been to listen for gestures on the collection view and if disable scrolling once a swipe occurs on the element. The obvious problem with this is that the user can simple hold and drag any particular cell.
Any thoughts?
I think this behavior may be confusing for your users.
Maybe you should try to add some elasticity/bouncing so that your users would be less confused.
Anyway, I see two different ways to achieve this without subclassing
1/ Since UICollectionViewDelegate conforms to UIScrollViewDelegate, you can get the starting offset of your scrollview with – scrollViewWillBeginDragging: then in – scrollViewDidScroll: you would compare the new offset's x value. If the new offset.x 's value is smaller than the starting one, set it to 0 and update your scrollview.
#pragma mark - UIScrollViewDelegate
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
offset = scrollView.contentOffset;
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGPoint newOffset = scrollView.contentOffset;
if (newOffset.x < offset.x) {
// scrolling to the left, reset offset
[scrollView setContentOffset:offset];
}
}
Because there is inertia with scrolling in iOS, scrollViewDidScroll: is called a lot of time, so it may cause performance issues. You may reduce the number of call by targeting your offset with scrollViewWillEndDragging:withVelocity:targetContentOffset: from UIScrollViewDelegate.
2/ Or ou can just use the method scrollViewWillEndDragging:withVelocity:targetContentOffset: which I just spoke about, which sets the offset back to its beginning, with an animation.
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
CGPoint newOffset = scrollView.contentOffset;
if (newOffset.x < offset.x) {
// scrolling to the left, reset offset with animation
targetContentOffset->x = offset.x;
}
}
3/ You spoke of UISwipeGestureRecognizer, did you give a try to UIPanGestureRecognizer? This is what "simple hold and drag" is.
You can implement it the same way you would implement an infinite scroll view, by adding / removing items based on the scroll offset.
Override the viewDidScroll: method (UICollectionViewDelegate)
Check if your offset puts the first object of your list past the offset (ie. can you still see it on the screen?)
If it is, then remove it from the collection view.
This simple implementation might result in choppy animations, you might have to do some optimization once it's working but this should get you started.
Another possible solution would be to "reposition" all your elements constantly to appear where they were before you started scrolling if you are scrolling left.
You can achieve this by keeping track of the highest offsetX you ever encountered, and reposition your cells if the current offsetX is lower than the max. That way you will have the impression that your cells are not moving or that you can't scroll, but you will actually be scrolling.
In my case, I have a paginated collection view, so I have to take care when decelerating in the last item of the collection view as well in automatic decelerations for each page.
To fix the issue, I just disable user interaction while the collection view is "moving automatically".
Here the code:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (!_isDragging) // <-- only do things if the user is dragging!
return;
CGPoint contentOffset = scrollView.contentOffset;
// If we move to the left
if (contentOffset.x < _contentOffset.x)
{
CGSize contentSize = scrollView.contentSize;
// If content offset is moving inside contentSize:
if ((contentOffset.x + scrollView.bounds.size.width) < contentSize.width)
scrollView.contentOffset= _contentOffset;
}
else
{
// Update the current content offset
_contentOffset = contentOffset;
}
}
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
_isDragging = YES;
}
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
_isDragging = NO;
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
if (decelerate)
{
// If willDecelerate, stop user interaction!
_collectionView.userInteractionEnabled = NO;
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
// Once deceleration is finished, enbale user interaction
_collectionView.userInteractionEnabled = YES;
// Set the new content offset
_contentOffset = scrollView.contentOffset;
}

Make 2 UIScrollViews zoom and scroll identically

I have a screen which has two UIScrollViews. Inside the scrollview is a zoomable UIImageView.
I'd like to achieve the same scrolling and zooming inside one scrollView to be applied to the other one. i.e. if the user pans across the image, both scrollviews pan their images at the exact same rate. If the user pinch zooms the image in one, the other zooms exactly the same amount.
I've read on here about using the zoomToRect:animated: call. I'm not sure exactly how to implement that, so I've tried the following - but it doesn't seem to yield the right results. NB. the scrollView contain self.imageViewLeft. self.scrollViewRight is the scrollView not being touched.
- (void)scrollViewDidZoom:(UIScrollView *)scrollView {
CGRect visibleRect = [scrollView convertRect:scrollView.bounds toView:self.imageViewLeft];
[self.scrollViewRight zoomToRect:visibleRect animated:false];
}
I solved it guys! Props to me.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView == self.scrollViewLeft) {
[self.scrollViewRight setZoomScale:[scrollView zoomScale]];
[self.scrollViewRight setContentOffset:[scrollView contentOffset]];
} else {
[self.scrollViewLeft setZoomScale:[scrollView zoomScale]];
[self.scrollViewLeft setContentOffset:[scrollView contentOffset]];
}
}

Resources