scrollViewDidScroll not called -- scrollViewDidEndDecelerating is called - cocoa-touch

I have a custom view replacing the keyboard and I'm trying to get it to scroll offscreen when the user scrolls down.
My original scrollViewDelegate methods worked EXCEPT there was a delay between user scrolling and view animation because I was using scrollViewDidEndDecelerating and it took about 1 second for this to be called after the user started scrolling.
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
self.isUserScrolling = YES;
// Save to tell if scrolling up or down
initialContentOffset = scrollView.contentOffset.y;
previousContentDelta = 0.f;
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
self.isUserScrolling = NO;
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
self.isUserScrolling = NO;
// If scrolled down
CGFloat prevDelta = previousContentDelta;
CGFloat delta = scrollView.contentOffset.y - initialContentOffset;
if (delta > 0.f && prevDelta <= 0.f) {
} else if (delta < 0.f && prevDelta >= 0.f) {
[self animateKeyBoardSpace:[self rectForKeyboardSpace:NO] curve:UIViewAnimationCurveEaseInOut duration:0.25];
}
previousContentDelta = delta;
}
So I am trying to now check for downward scrolling in scrollViewDidScroll and call animateKeyBoardSpace there like so:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
// ScrollDirection scrollDirection;
if (lastContentOffset > scrollView.contentOffset.y) {
[self animateKeyBoardSpace:[self rectForKeyboardSpace:NO] curve:UIViewAnimationCurveEaseInOut duration:0.25];
}
else if (lastContentOffset < scrollView.contentOffset.y) {
}
lastContentOffset = scrollView.contentOffset.x;
}
HOWEVER, scrollViewDidScroll isn't even getting called. It's in the same tableViewController, the delegate is set, and the other delegate methods are getting called.

I was working in the superclass and I had a stub for scrollViewDidScroll in the subclass from some time ago. This was capturing the delegate call.
I just had to delete the subclass stub.

Related

Zoom simultaneously in two UIImageView

I need to have two UIImageView, both with zoom enabled, (I can use the typical UISCrollView technique) and whenever I zoom in in one of them will result to zoom in the other as well, in the same point and with the same zoom.
Here's an explicative video: https://youtu.be/Ft-Dt3fx3z8?t=29s
Do you know any solution?
Thanks
I will answer my own question, first solution zooming async without scrolling
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
if (scrollView == _scrollView1)
return self.imageView1;
else
return self.imageView2;
}
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)scale
{
if (scrollView == _scrollView1){
[_scrollView2 setZoomScale:scale animated:YES];
[_scrollView2 setContentOffset:_scrollView1.contentOffset animated:YES];
}
else if (scrollView == _scrollView2){
[_scrollView1 setZoomScale:scale animated:YES];
[_scrollView1 setContentOffset:_scrollView2.contentOffset animated:YES];
}
}
Second, better solution with real time zooming and scrolling (viewForZoomingInScrollView is still needed)
- (void)matchScrollView:(UIScrollView *)first toScrollView:(UIScrollView *)second {
CGPoint offset = first.contentOffset;
offset.x = second.contentOffset.x;
offset.y = second.contentOffset.y;
[first setContentOffset:offset];
[first setZoomScale:second.zoomScale];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if([scrollView isEqual:_scrollView1]) {
[self matchScrollView:_scrollView2 toScrollView:_scrollView1];
} else {
[self matchScrollView:_scrollView1 toScrollView:_scrollView2];
}
}

UIScrollView disable vertical bounce only at bottom

I've been searching for a way to disable vertical bounce for a UIScrollView but only at bottom. I still need to have the bounce at the top.
Couldn't find anything. Is this possible?
Thanks in advance!
Use the UIScrollViewDelegate method scrollViewDidScroll to check the content offset of the scrollview and more or less check if the user is trying to scroll beyond the bottom bounds of the scroll view. Then just use this to set the scroll back to the end of the scroll view. It happens so rapidly that you can't even tell that the scroll view bounced or extended beyond its bounds at all.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView.contentOffset.y >= scrollView.contentSize.height - scrollView.frame.size.height) {
[scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, scrollView.contentSize.height - scrollView.frame.size.height)];
}
}
To disable vertical bounce at the top in Swift:
func scrollViewDidScroll(scrollView: UIScrollView) {
if scrollView.contentOffset.y < 0 {
scrollView.contentOffset.y = 0
}
}
To disable vertical bounce at the bottom in Swift:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y > scrollView.contentSize.height - scrollView.bounds.height {
scrollView.contentOffset.y = scrollView.contentSize.height - scrollView.bounds.height
}
}
Based on 0x7fffffff's answer I've created a class:
VerticalBounceControl.h
#import <UIKit/UIKit.h>
#interface VerticalBounceControl : NSObject
#property (nonatomic, assign) BOOL bounce;
- (id) initWithScrollView:(UIScrollView*)scrollView bounceAtTop:(BOOL)bounceAtTop bounceAtBottom:(BOOL)bounceAtBottom;
#end
VerticalBounceControl.m
#import "VerticalBounceControl.h"
#interface VerticalBounceControl ()
#property (nonatomic, retain) UIScrollView* scrollView;
#property (nonatomic, assign) BOOL bounceAtTop;
#property (nonatomic, assign) BOOL bounceAtBottom;
#end
#implementation VerticalBounceControl
- (id) initWithScrollView:(UIScrollView*)scrollView bounceAtTop:(BOOL)bounceAtTop bounceAtBottom:(BOOL)bounceAtBottom {
self = [super init];
if(self) {
self.bounce = YES;
self.scrollView = scrollView;
self.bounceAtTop = bounceAtTop;
self.bounceAtBottom = bounceAtBottom;
[self.scrollView addObserver:self forKeyPath:#"contentOffset" options:NSKeyValueObservingOptionNew context:NULL];
}
return self;
}
- (void) dealloc {
[self.scrollView removeObserver:self forKeyPath:#"contentOffset"];
self.scrollView = nil;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([keyPath isEqualToString:#"contentOffset"]) {
if (self.bounce && ((self.scrollView.contentOffset.y<=0 && self.bounceAtTop) || (self.scrollView.contentOffset.y>=self.scrollView.contentSize.height-1 && self.bounceAtBottom))) {
self.scrollView.bounces = YES;
} else {
self.scrollView.bounces = NO;
}
}
}
#end
that can be used without the caller being a delegate of UIScrollView like this:
VerticalBounceControl* bounceControl = [[VerticalBounceControl alloc] initWithScrollView:anyScrollView bounceAtTop:YES bounceAtBottom:NO];
This way you can have this bounce behavior for UIScrollViews that you don't want to alter their delegate (like UIWebView's one).
Again, thanks go #0x7fffffff for the solution!
Swift 3:
This works to disable vertical bounce only at the bottom:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.contentOffset.y > scrollView.contentSize.height - scrollView.bounds.height {
scrollView.contentOffset.y = scrollView.contentSize.height - scrollView.bounds.height
}
}
Let's disable the bounces when scroll to the bottom.
scrollView.bounces = scrollView.contentOffset.y < scrollView.contentSize.height - scrollView.frame.height
I added fabsf() to 0x7fffffff♦'s solution to also cater for negative(downward) scroll:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (fabsf(scrollView.contentOffset.y) >= scrollView.contentSize.height - scrollView.frame.size.height) {
[scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, scrollView.contentSize.height - scrollView.frame.size.height)];
}
}
I have been looking a solution to disable bottom bounce only for a scrollview with wider contentSize width than device since I am using paging. None of those work for me but I found a way to do it. I thought I should share this since I wasted two days just to do this.
I am using Swift 4 but this applies to objective c if you know how to convert this which should be easy if you have experience with both.
This works in a scrollview with device screen frame as frame.
Add variable to be aware for scrollview's current page.
var currentPage : Int!
Set initial value
currentPage = 0
Update currentPage after scrolling
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let width = scrollView.frame.size.width
let page = (scrollView.contentOffset.x + (0.5 * width)) / width
currentPage = Int(page)
}
Filter bottom scrolling and update contentOffset
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
if CGFloat(currentPage) * screenWidth < scrollView.contentOffset.x || CGFloat(currentPage) * screenWidth < scrollView.contentOffset.y || (((CGFloat(currentPage) * screenWidth - scrollView.contentOffset.x) >= 0) && scrollView.contentOffset.y > 0){
scrollView.contentOffset.y = 0
}
}
CGFloat(currentPage) * screenWidth < scrollView.contentOffset.x || CGFloat(currentPage) * screenWidth < scrollView.contentOffset.y will filter bottom left and right scrolling
(((CGFloat(currentPage) * screenWidth - scrollView.contentOffset.x) >= 0) && scrollView.contentOffset.y > 0) will filter bottom mid scrolling
scrollView.contentOffset.y = 0 will stop it from scrolling

How do I use two actions in a UIPanGestureRecognizer?

I am working with two subviews. Each will be unique and have it's own "action".
Subview 1 = User can drag around the view, rotate, and zoom it
Subview 2 = When user moves finger across their screen an image is added at each point their finger touches.
I have both of these completed by using UIPanGestureRecognizer. My question is, how can I separate these two actions? I want to be able to add one subview, do what is required, and then when I add the other subview, prevent the previous actions from occurring.
Here is what I have tried, this is done in my panGesture method:
for (UIView * subview in imageView.subviews)
{
if ([subview isKindOfClass:[UIImageView class]])
{
if (subview == _aImageView)
{
CGPoint translation = [panRecognizer translationInView:self.view];
CGPoint imageViewPosition = _aImageView.center;
imageViewPosition.x += translation.x;
imageViewPosition.y += translation.y;
_aImageView.center = imageViewPosition;
[panRecognizer setTranslation:CGPointZero inView:self.view];
}
else if (subview == _bImageView)
{
currentTouch = [panRecognizer locationInView:self.view];
CGFloat distance = [self distanceFromPoint:currentTouch ToPoint:prev_touchPoint];
accumulatedDistance += distance;
CGFloat fixedDistance = 60;
if ([self distanceFromPoint:currentTouch ToPoint:prev_touchPoint] > fixedDistance)
{
[self addbImage];
prev_touchPoint = currentTouch;
}
}
}
}
If you want different gesture recognition in two different views, put separate recognizers on each view.
Usually, you want to have your view controller own and manage gesture recognizers, e.g.
- (void)viewDidLoad {
self.panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handleGesture:)];
self.panGesture.delegate = self;
[self.viewX addGestureRecognizer:self.panGesture];
// repeat with other recognisers...
}
Note that setting your controller as delegate of the gestureRecognizer is important: this enables you to handle the following delegate method from the view controller (which was the main question):
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
// handle your logic, which gestureRecognizer should proceed...
return NO;
}
The handler method is the same is this example, but you can set up your own handlers as you like:
- (void)handleGesture:(UIGestureRecognizer*)gestureRecognizer {
// handle gesture (usually sorted by state), e.g.
// if(gesture.state == UIGestureRecognizerStateEnded) { ... }
}

How to find out if scrollView is about to be scrolled up or down

I would like to find out if a scrollView is scrolled up or down. Ideally, I'd like to have only one call if the scrollView is scrolled up or down. I tried this but it will obviously not tell me anything about the direction:
-(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
NSLog(#"%.2f", scrollView.contentOffset.y);
}
contentOffset will always be 0 - it doesn't matter whether I scrolled up or down. Now I could simply check in -(void)scrollViewDidScroll: if the offset is positive or negative, but this is called constantly. scrollViewWillBeginDragging has the advantage of being called only once and this is what I need. Is there something like scrollViewDidBeginDragging? I didn't find anything in the docs. Any smart workaround?
Store the initial content offset in scrollViewWillBeginDragging:
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
self.initialContentOffset = scrollView.contentOffset.y;
self.previousContentDelta = 0.f;
}
And check it on each scrollViewDidScroll:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
CGFloat prevDelta = self.previousContentDelta;
CGFloat delta = scrollView.contentOffset.y - self.initialContentOffset;
if (delta > 0.f && prevDelta <= 0.f) {
// started scrolling positively
} else if (delta < 0.f && prevDelta >= 0.f) {
// started scrolling negatively
}
self.previousContentDelta = delta;
}
It IS possible to perform this check in scrollViewWillBeginDragging before any scrolling is registered. (IOS 5+). By examining the scroll view's built-in pan gesture recognizer you can check gesture direction.
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
CGPoint translation = [scrollView.panGestureRecognizer translationInView:scrollView.superview];
if(translation.y > 0)
{
// react to dragging down
} else
{
// react to dragging up
}
}
I found it very useful in canceling out of a scroll at the very first drag move when the user is dragging in a forbidden direction.
Create a declared property to let us know that the tableview is starting to scroll. Let's use a BOOL called scrollViewJustStartedScrolling.
In scrollViewWillBeginDragging set it to true:
self.scrollViewJustStartedScrolling = YES;
In scrollViewDidScroll do something like:
if (self.scrollViewJustStartedScrolling) {
// check contentOffset and do what you need to do.
self.scrollViewJustStartedScrolling = NO;
}
-(void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{
if (velocity.y > 0){
NSLog(#"up");
} else {
NSLog(#"down");
}
}

iPhone OS3 changes to UIScrollView subclasses

I have a subclass of UIScrollView that overrides
touchesBegan:withEvent:
touchesMoved:withEvent:
touchesEnded:withEvent:
Overriding these three seems to be a technique that is widely used (based on my observations in forums). However, as soon as I compiled this code on OS3, these methods are no longer being called. Has anyone else seen this problem? Is there a known fix that doesn't use undocumented methods?
My first attempt at a solution was to move all the touchesBegan/Moved/Ended methods down into my content view and set
delaysContentTouches = NO;
canCancelContentTouches = NO;
This worked partially, but left me unable to pan when I have zoomed. My second attempt only set canCancelContentTouches = NO when there were two touches (thus passing the pinch gesture through to the content). This method was sketchy and didn't work very well.
Any ideas? My requirement is that the scroll view must handle the pan touches, and I must handle the zoom touches.
My solution is not pretty. Basically there is a scroll view who contains a content view. The scroll view does not implement touchesBegan,Moved,Ended at all. The content view maintains a pointer to his parent (called "parentScrollView" in this example). The content view handles the logic and uses [parentScrollView setCanCancelContentTouches:...] to determine whether or not to let the parent view cancel a touch event (and thus perform a scroll event). The tap count logic is there because users rarely place both fingers onscreen at exactly the same time so the first touch must be ignored if it is very quickly followed by a second.
-(void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event
{
if(parentViewIsUIScrollView)
{
UIScrollView * parentScrollView = (UIScrollView*)self.superview;
if([touches count] == 1)
{
if([[touches anyObject] tapCount] == 1)
{
if(numberOfTouches > 0)
{
[parentScrollView setCanCancelContentTouches:NO];
//NSLog(#"cancel NO - touchesBegan - second touch");
numberOfTouches = 2;
}
else
{
[parentScrollView setCanCancelContentTouches:YES];
//NSLog(#"cancel YES - touchesBegan - first touch");
numberOfTouches = 1;
}
}
else
{
numberOfTouches = 1;
[parentScrollView setCanCancelContentTouches:NO];
//NSLog(#"cancel NO - touchesBegan - doubletap");
}
}
else
{
[parentScrollView setCanCancelContentTouches:NO];
//NSLog(#"cancel NO - touchesBegan");
numberOfTouches = 2;
}
//NSLog(#"numberOfTouches_touchesBegan = %i",numberOfTouches);
}
}
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
if(touchesCrossed)
return;
if(parentViewIsUIScrollView)
{
UIScrollView * parentScrollView = (UIScrollView*)self.superview;
NSArray * thoseTouches = [[event touchesForView:self] allObjects];
if([thoseTouches count] != 2)
return;
numberOfTouches = 2;
/* compute and perform pinch event */
[self setNeedsDisplay];
[parentScrollView setContentSize:self.frame.size];
}
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
touchesCrossed = NO;
if(parentViewIsUIScrollView)
{
numberOfTouches = MAX(numberOfTouches-[touches count],0);
[(UIScrollView*)self.superview setCanCancelContentTouches:YES];
//NSLog(#"cancel YES - touchesEnded");
//NSLog(#"numberOfTouches_touchesEnded = %i",numberOfTouches);
}
}