UIScrollView - Figuring out where the scroll will stop - iphone

I'm trying to figure out how to calculate where the scrollview will stop when a user does a swipe gesture and the scrollview goes into deceleration. I'm trying to use the delegate functions, but I can't accurately figure it out. Please help!
- (void) scrollViewDidScroll:(UIScrollView *)scrollView;
- (void) scrollViewWillBeginDecelerating:(UIScrollView *)scrollView;
- (void) scrollViewDidEndDecelerating:(UIScrollView *)scrollView;

This thread is a bit old, but still comes up top in a search for this problem.
The UIScrollViewDelegate protocol contains the following method that tells your code where the scroll view is expected to stop...
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
The targetContentOffset parameter is an inout so if you set it to a different value, the scrollview will actually come to a stop at the offset you specify.

Unfortunately UIScrollView doesn't work this way-- there's no way to ask it up front where the deceleration ends.

Sounds complicated. The user may start scroll again while it is decelerating, stop it suddenly or what not. Maybe you can get by with just doing DidEndDecelerating, i.e just detecting the position when the scrolling is over?

It should be simple math. The contentOffset property of the scrollview is updated each time scrollViewDidScroll is called. You only need to save the vector between two positions and the time to get at least a descent end-position of the deceleration.
Note that the user can stop the deceleration any time by tapping on the scrollview as Jaanus pointed out.

There is a variable for the current acceleration that is kept in the UIScrollView implementation but using that as a solution is impossible because you can't compile the code for a device.

Related

UIScrollView Notification?

Is there any notification sent when UIScrollView changes its scroll state? I would like to listen to that notification rather than using delegate methods.
If you don't want to use delegate methods you can observe contentOffset value changes using KVO (key-value-observing)
You can subclass UIScrollView, overload touchesMoved:withEvent:, and send this notification every time your scroll view will scroll.
As the scrolls need to be lightweight events I would not recommend using notifications for every scroll as it will impact your performance much greater than using a delegate method.
These are the delegates that handle scrolling for UIScrollView.
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
Is there any specific reason you dont prefer delegates & want notifications? So you can use scrollViewDidScroll for any changes in scroll state.

UIScrollView notifications

I'm coding an app that works much like Apple's Weather.app: There's a UIPageControl at the bottom and a UIScrollView in the middle of the screen.
In my code, I implemented the - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView method to figure out when the user did move to a new page. If they move to a new page, I load the adjacent pages' data, as to make further page-switching faster. (In one of Apple's examples, the - (void)scrollViewDidScroll:(UIScrollView *)sender is used, but that causes my app to shortly hang when loading a new page, so it's not suitable.)
That code works very well.
I'm using scrollRectToVisible:: to programmatically scroll inside the scrollview when the user clicks the UIPageControl. The problem is that the scrollRectToVisible: doesn't post a notification to the UIScrollViewDelegate when it's done scrolling - so the code responsible for loading adjacent pages never get's called when using the UIPageControl.
Is there any way to make the UIScrollView notify its delegate when it gets called by the scrollRectToVisible: method? Or will I have to use threads in order to prevent my app from freezing?
Thanks!
-- Ry
How about -scrollViewDidEndScrollingAnimation:?
If it doesn't work, try to listen to the UITextSelectionDidScroll notification. (Of course, it's undocumented.)
Alternatively, an SDK-safe method is measure the time taken for the animation and send a delayed notification at the call site of -scrollRectToVisible:.
You could add this delegate method instead:
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView
The scroll view calls this method at the end of its implementations of the UIScrollView and setContentOffset:animated: and scrollRectToVisible:animated: methods, but only if animations are requested.

Implement custom zooming for a UIScrollView

I have a UIScrollView with the requirement that, when zooming, the contentSize.height should remain the same. Zooming in from 200x100 should result in a new contentSize of 400x100 instead of 400x200, for instance. I'd like to do my own drawing while the user is zooming.
I don't think I can use the normal zooming behaviour of UIScrollView to achieve this, so I'm trying to roll my own. (I could just let it do its thing and then redraw my contents when -scrollViewDidEndZooming:withView:atScale: gets called, but that wouldn't be very pretty).
Currently I am subclassing UIScrollView and trying to do my own zooming when two fingers are on the screen:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
if ([touches count] != 2) {
[super touchesMoved:touches withEvent:event];
} else {
// do my own stuff
}
}
I thought that by overriding touchesBegan:withEvent:, touchesMoved:withEvent:, touchesEnded:withEvent: and touchesCancelled:withEvent: in this way should work, but it doesn't.
An earlier failed attempt was to place a transparent view on top of the scrollview and send touches that I'm not interested in to the scrollview :
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
if ([touches count] != 2) {
[self.theScrollView touchesMoved:touches withEvent:event];
} else {
// do my own stuff
}
}
Any help would be appreciated.
Thanks,
Thomas
You probably won't be able to maintain decent performance during zooming if you attempt to redraw your content on every frame of a pinch-zooming event. I'd recommend taking the approach of letting the UIScrollView zoom a scaled version of your drawing in or out, then redraw the content to be sharp at the end of the zoom in the -scrollViewDidEndZooming:withView:atScale: delegate method. This is what I do in my application, and it ends up working very well.
There are some tricks to resizing your content view properly at the end of a zoom, which I describe in this answer. Basically, you need to intercept the setting of a transform to your content view so that you can set it to a scale factor of 1 when you redraw your content. You'll also need to keep track of that scale factor, because the UIScrollView doesn't, and use that scale factor to adjust the transform that UIScrollView tries to apply to your content view with subsequent zoom operations.
You could use a modification of this technique to force a redraw of your content during the pinch-zooming, but in my tests this ended up being far too jerky to provide a good user experience.
I'm not sure what you mean by this:
I thought that by overriding
touchesBegan:withEvent:,
touchesMoved:withEvent:,
touchesEnded:withEvent: and
touchesCancelled:withEvent: in this
way should work, but it doesn't.
Do you not get the events? You should receive the events, but I think there is a logic error in your if statement that may have been preventing this from working.
if ([touches count] != 2)
This is a problem, because the likelihood of the two touches happening precisely the same time is low. You'll want to be able to handle when touches happen independently, as well as when a user holds a finger stationary, and moves the other one. In these scenarios (which are common) you may only get one touch in that NSSet, even though the other is still valid.
The solution to handling touches properly is to remember some things about which touches came in and which touches left. Remember, the address of the UITouch does not change for the life of the touch, so you can safely compare addresses to ensure you are still dealing with the same touch as before, and track it's lifecycle.
If you are not getting the touch events, then that is a different problem altogether, you may need to turn set multiTouchEnabled:YES
I'm trying to do the same thing as this and I really want to be able to redraw while it's zooming. Fixing it up at the end in scrollViewDidEndZooming:withView:atScale is not really good enough.
The way I do it is pass a dummy view in viewForZoomingInScrollView: and read the height of this dummy view and set the frame on the actual content view to whatever I want. Because the frame changes, it means that drawRect gets called everytime. It seems fine on the simulator, I'm just drawing a few lines. But I don't actually own a device though, so I can't test it properly.
Also in the code you've got, you have touchesBegan:withEvent: but then you are forwarding to super touchesMoved:withEvent: instead of touchesBegan:withEvent:

Being informed when a zoomToRect: animation completes

In the iPhone 3.0 SDK, how can I be informed when a zoomToRect:animated: animation completes?
Sometimes the scrollview doesn't zoom at all (if it's already at the proper zoom level), and there doesn't seem to be a way to detect that.
On your delegate, you can listen for - (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale
see UIScrollView and UIScrollViewDelegate
The easiest way would probably to put in a check before starting the animation. If the zoom level is already correct, call [self performSelectorOnMainThread:#selector(myMethod) withObject:id waitUntilDone:NO].
You won't be able to use the same method as scrollViewDidEndZooming, because you can only pass one argument, but it will trigger an asynchronous call on the main thread

How do I choose the correct view for zooming with multiple UIScrollView objects in a view (iPhoneSDK obj-C)?

I have added several UIScrollViews as subviews of a single UIView and set the frames so that each one is clearly visable. I set scrollEnabled to YES and set the contentSize larger than the bounds/frame. I do this in a for loop, and with each pass of the loop I release the UIScrollView (though the object is still stored because it has been subviewed into the UIView). This works well for being able to scroll around the imageView stored in each particular UIScrollView but I cannot for the life of me figure out how to get the zoom to work. I included the in the interface. Here are the methods I have tried for choosing the correct view for zooming:
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView{
return [[myView subviews] objectAtIndex:pageNum];
}
and
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView{
return [myView viewWithTag:pageNum];
}
neither seems to work. The weird part is that scrolling works fine. I can't even get the viewForZooming method to get called at all if I put in an NSLog call. Any ideas? I think I've lost all my hair from getting frustrated with this.
Edit: Thanks a lot cduhn! All I needed was that little bump, I had forgotten to set the scrollView delegate to self... I've been working with various apps that take advantage of UIScrollView for months now and been using the delegate correctly and this most recent one I don't know where my brain went.
However, you do not need to override the scrollViewDidEndZooming:withView:atScale:, the delegate will call that no matter what after a zoom.
Also, after a little tweeking this worked:
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView{
return [[[myView viewWithTag:pageNum] subviews] objectAtIndex:0];
}
This simply calls the scrollView inside View container and then gets the UIImage inside of that... works well.
It sounds like you may not have set the delegate property on your UIScrollViews to point at the object that implements viewForZoomingInScrollView:
Also note this snippet from the UIScrollView Class Reference:
For zooming and panning to work, the delegate must implement both viewForZoomingInScrollView: and scrollViewDidEndZooming:withView:atScale:; in addition, the maximum (maximumZoomScale) and minimum ( minimumZoomScale) zoom scale must be different.
Finally, a word of warning: Be careful when accessing the subviews of UIScrollView. Your subviews are not alone in there. UIScrollView adds its own UIImageViews as subviews of itself to implement its scrollbar UI. So code like this...
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
return [[myView subviews] objectAtIndex:pageNum];
}
... may not do what you expect.